AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Multi Tenancy

skill-deadlymind-nanolama-multi-tenancy · by Deadlymind

Enforces tenant isolation on a Django/DRF app where every business model carries a non-null tenant FK (entreprise) and ViewSets auto-filter fail-closed by the current tenant. Use when adding a tenant-scoped model, writing or reviewing a ViewSet or queryset, wiring a TenantScopedViewSet mixin, fixing a cross-tenant data leak, or asking how entreprise scoping works here. Not for role/permission gat…

No reviews yet
0 installs
18 views
0.0% view→install

Install

$ agentstack add skill-deadlymind-nanolama-multi-tenancy

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-deadlymind-nanolama-multi-tenancy)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
24d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Multi Tenancy? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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:

  1. Every business model carries a non-null tenant FK (entreprise). No nulls,

no "global" business rows.

  1. 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.

  1. 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

  1. 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 ``

  1. Subclass one fail-closed mixin instead of hand-writing get_queryset per 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) ```

  1. Object lookups reuse the scoped queryset, so per-object 404 is automatic —

never Model.objects.get(pk=...) in a view (that bypasses scoping).

  1. 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_id is 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/tenant id 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 @action methods 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-permissions
  • drf-api
  • migrations
  • security-review
  • write-tests

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.