Install
$ agentstack add skill-deadlymind-nanolama-multi-tenancy ✓ 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 No
- ✓ 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
Multi-tenancy (fail-closed tenant isolation)
When to use
Adding or reviewing any business model or API that must never expose one tenant's rows to another. On this stack, tenant isolation is the single most important invariant — treat a missing tenant filter as a security bug, not a style nit.
Pattern
Three rules, held everywhere:
- Every business model carries a non-null tenant FK (
entreprise). No nulls,
no "global" business rows.
- Reads are scoped in
get_queryset(), fail-closed. The tenant comes from the
authenticated user, never from the request body/query string. No tenant → return .none(), never all rows.
- Writes set the tenant server-side in
perform_create/perform_update, so a
client can never assign a row to another tenant.
Access within a tenant is a separate concern — gate it with rbac-permissions.
Steps / idioms
- Give the model a non-null tenant FK and a related name:
``python class Invoice(models.Model): entreprise = models.ForeignKey( "tenants.Entreprise", on_delete=models.CASCADE, null=False, related_name="invoices", ) # ... business fields class Meta: indexes = [models.Index(fields=["entreprise"])] # every filter hits it ``
- Subclass one fail-closed mixin instead of hand-writing
get_querysetper view:
```python # tenants/mixins.py class TenantScopedViewSet(viewsets.ModelViewSet): tenantfield = "entrepriseid" # override for indirect ownership
def getqueryset(self): user = self.request.user tenantid = getattr(user, "entrepriseid", None) if not user.isauthenticated or tenantid is None: return super().getqueryset().none() # fail closed return super().getqueryset().filter(**{self.tenantfield: tenant_id})
def perform_create(self, serializer): serializer.save(entreprise=self.request.user.entreprise) ```
- Object lookups reuse the scoped queryset, so per-object 404 is automatic —
never Model.objects.get(pk=...) in a view (that bypasses scoping).
- Add a tenant-isolation test for every scoped resource (see
write-tests):
user A must get 404/empty for user B's object.
Variants
Keep the invariant; change only what "the current tenant" resolves to.
- One entreprise = one company (default).
user.entreprise_idis the tenant. - Indirect ownership (tenant reached through a parent). Some models have no direct
tenant FK — they belong to one through a parent row. Make the mixin field configurable with a class attribute (tenant_field = "entreprise_id" by default) and filter on it (.filter(**{self.tenant_field: tenant_id})). For an indirectly owned model, set the traversal path, e.g. tenant_field = "parent__entreprise". The fail-closed .none() and perform_create stamping (via the parent) still apply — and index the parent FK the path joins on, or every scoped read scans.
- Sub-teams / departments inside a tenant. Tenant FK still gates isolation; add a
second, optional team/departement FK and layer it as an RBAC/visibility filter on top of the tenant filter — never as a replacement for it.
- Agency operating over client sub-accounts. Model the client as the tenant and
give agency users an explicit, audited membership across several entreprise rows; resolve "current tenant" from an X-Tenant-style selector validated against that membership, then filter by it. Still fail-closed: an unknown/foreign tenant → .none().
Adapt to your repo
Rename Entreprise/entreprise, the accessor path (user.entreprise_id vs user.profile.entreprise_id), and the app label to match your project. If your tenant is resolved from a request header (agency variant), validate it against server-side membership before trusting it. Confirm the FK is null=False in the migration.
Gotchas
- A client-supplied
entreprise/tenantid in the body or query string is never
trusted — scope from request.user only.
.none()on the empty/anonymous case, not an unfiltered queryset — fail closed.- Custom
@actionmethods and nested/related lookups need the same scoping; the
mixin only covers the default queryset (see rbac-permissions to gate actions).
- The tenant filter prevents leaks, not N+1 — add
select_related/prefetch_related
in the serializer (see perf-review).
See also
rbac-permissionsdrf-apimigrationssecurity-reviewwrite-tests
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.