Multi-tenant enforcement engine for Python. Prevents cross-tenant data leaks by construction.
Project description
TenantShield
Multi-tenant enforcement engine for Python — prevent cross-tenant data leaks by construction, not convention.
The Problem
Multi-tenant SaaS applications can leak data across tenant boundaries
when a single forgotten .filter(tenant_id=...) slips into production.
The leak is silent: queries succeed, responses look normal, and the
breach is often detected only when affected tenants report seeing
data that isn't theirs.
The root cause is structural: tenant scoping is typically enforced by convention (developers remembering to filter), not by the system. One missed filter, one new query path, one ORM method that bypasses the scoped manager — and the boundary is gone.
Before TenantShield
# Django: tenant scoping by convention -- one forgotten filter leaks.
class InvoiceView(View):
def get(self, request):
# Correct: scoped to current tenant
invoices = Invoice.objects.filter(tenant_id=request.tenant.id)
return JsonResponse({"invoices": list(invoices.values())})
def export_all(self, request):
# LEAK: no tenant filter -- returns ALL tenants' invoices.
invoices = Invoice.objects.all()
return JsonResponse({"invoices": list(invoices.values())})
The bug compiles. Tests pass unless they explicitly cover cross-tenant isolation. Production traffic returns wrong-tenant data. No exception is raised.
After TenantShield
# Same model, with TenantShield: scoping is enforced by the system.
from tenantshield.adapters.django import tenant_aware
@tenant_aware
class Invoice(models.Model):
tenant_id = models.CharField(max_length=64)
amount = models.DecimalField(max_digits=10, decimal_places=2)
# ... rest of model
class InvoiceView(View):
def get(self, request):
invoices = Invoice.objects.all() # auto-scoped to request.tenant
return JsonResponse({"invoices": list(invoices.values())})
def export_all(self, request):
# STILL auto-scoped -- no way to forget.
invoices = Invoice.objects.all()
return JsonResponse({"invoices": list(invoices.values())})
Outside a tenant context (e.g., a misconfigured background worker), the
same query raises MissingTenantContextError instead of silently
returning unscoped data. Cross-tenant writes raise
CrossTenantAccessError before the SQL executes.
What was a convention developers had to remember at every call site becomes a constraint the system enforces by default.
TenantShield is a layer over existing ORMs (Django, SQLAlchemy) and request
frameworks (Django, FastAPI, Flask, any ASGI/WSGI application) that enforces
tenant scoping automatically. The default policy is deny-by-default: any
query against a tenant-aware model without an explicit tenant context fails
loudly. Cross-tenant writes are detected and rejected. Tenant context
propagates across async boundaries via Python's native contextvars.
Adoption is incremental — adapters are activated explicitly and there is no import-time monkey-patching.
Status
🔵 Beta — version 0.7.0b0. The architectural arc from foundation
through production hardening + first cohort-validated maturity is
complete (Phases 0 → 6). The Django adapter is validated in production
by a reference adopter (Counterbook, ~196 commits over 6 sprint cycles,
0 architectural findings introduced). SQLAlchemy and DRF adapters are
functional with known limitations documented in the adapters guide;
Celery integration is on the roadmap,
not yet shipped.
Distribution: install from PyPI with pip install --pre tenantshield
(pre-releases require --pre; pip install tenantshield without --pre
will not install beta versions). TestPyPI continues in parallel for
release-candidate testing. A v1.0.0 stable GA release is planned once
a second adapter is validated in production and the Phase 7 architectural
gaps are closed.
Features
Framework adapters
- Django ORM (validated in production by reference adopter, 6 sprint cycles, 0 findings):
tenant_awaremanager + signal enforcement +TenantContextMiddleware. - Django REST Framework (functional; test coverage expanding): triple defense (permissions + viewset mixin + serializer validation).
- SQLAlchemy 2.x sync + async (functional; Phase 7 gaps documented —
_unsafe_unscopedandauto_propagate_from_parent_fkparity with Django pending; see Known Limitations):@tenant_awaredeclarative decorator + event-based write enforcement (before_insert/before_update/before_delete) + read filtering viado_orm_execute+ scope managers (SessionScope/AsyncSessionScope). - ASGI middleware:
TenantSessionMiddleware(sync ctx mgr with dual-mode resolver) +AsyncTenantSessionMiddleware(async-native; Phase 5A) for FastAPI / Starlette / any ASGI 3.0 framework. - WSGI middleware:
TenantSessionMiddlewareWSGIwith generator-safe scope (Flask / Django WSGI / Gunicorn).
Core capabilities
- Tenant scope contracts:
tenant_scope(ctx)+atenant_scope(ctx)for explicit binding;bind_session_to_tenant/bind_async_session_to_tenantfor SA session binding. - Policy engine:
DenyByDefaultPolicy,AllowListPolicy,RequireScope,ChainPolicy. Composable. - Audit bus: pluggable sinks (
StructLogSink,InMemorySink,NullSink, or custom). 6AuditEventTypevalues (CONTEXT_BOUND/CONTEXT_RELEASED/POLICY_ALLOW/POLICY_DENY/ENFORCEMENT_VIOLATION/SINK_FAILURE). - Cross-adapter extraction strategies:
HeaderStrategy,HostStrategy,JWTStrategy,CallableStrategyoperating on a minimalRequestProtocolabstraction. Same strategy instance works across Django + ASGI.
Production hardening (Phase 5)
- Structured observability:
tenantshield.observabilitymodule with a 9-event taxonomy across scope lifecycle + enforcement + middleware boundaries. structlog-based emission with~6 ns/calldisabled-default gate. - Audit-observability dual-pattern: policy-level audit events + operation-
level observability events emit at independent gates (sink registry vs
is_enabled()flag). Decision 7-A separation verified empirically. - Adopter-extensible processor chain: OpenTelemetry trace context + Prometheus metrics integrate as adopter-prepended structlog processors; zero TenantShield-side coupling.
Install
Stage 1 — Local wheel distribution
TenantShield is currently distributed via local wheel/sdist artifacts to a validation cohort. To build and install from the repository:
uv build
pip install dist/tenantshield-0.5.0a0-py3-none-any.whl
Or install a provided wheel artifact directly:
pip install tenantshield-0.5.0a0-py3-none-any.whl
Adapter-specific extras:
pip install "tenantshield[django]" # Django + DRF adapter
pip install "tenantshield[sqlalchemy]" # SQLAlchemy adapter
pip install "tenantshield[jwt]" # JWT strategy support
pip install "tenantshield[drf]" # Django REST Framework adapter
Stage 2 — PyPI distribution (upcoming)
Public PyPI distribution is planned after validation cohort feedback.
Quickstart
The canonical minimum: register an audit sink, define a policy, enter a tenant scope, and evaluate operations.
from tenantshield import (
DenyByDefaultPolicy,
Operation,
OperationType,
StructLogSink,
TenantId,
bind_tenant,
evaluate_and_audit,
register_sink,
tenant_scope,
)
# 1. Register a sink so audit events go somewhere.
register_sink(StructLogSink())
# 2. Define a policy.
policy = DenyByDefaultPolicy()
# 3. Enter a tenant scope.
ctx = bind_tenant(TenantId("acme"))
with tenant_scope(ctx):
# 4. Evaluate an operation.
operation = Operation(
model="app.Invoice",
operation_type=OperationType.READ,
tenant_context=ctx,
)
decision = evaluate_and_audit(policy, operation)
print(decision) # Allow()
Outside a tenant scope, the same evaluation returns
Deny(reason="No tenant context active for read on 'app.Invoice'").
For framework-specific quickstarts (Django, SQLAlchemy + FastAPI, SQLAlchemy + Flask), see the Adapters documentation and the Examples below.
Enable observability (Phase 5, opt-in)
from tenantshield.observability import configure
configure(emit_events=True)
Disabled by default — the gate adds ~6 ns/call when off and zero log
volume. See Observability Quick Start
for adopter integration patterns.
Documentation
- Getting Started — install + minimal example.
- Concepts — building blocks (TenantContext, policies, audit bus, registry).
- API Reference — complete public surface.
- Adapters — Django + SQLAlchemy + middleware integration guides.
- Observability (docs/observability/):
- Quick Start — enable emission + configure structlog.
- Dual-Pattern — audit bus + observability semantics.
- Async Middleware Migration
- Production Checklist
- OpenTelemetry integration
- Prometheus integration
- Architectural Decision Records — 12 ADRs documenting design decisions.
- Changelog — release history with detailed Decision Records per phase.
Examples
Runnable adopter examples lives in examples/:
- FastAPI + SQLAlchemy — async ASGI
application with
TenantSessionMiddleware. - Flask + SQLAlchemy — sync WSGI
application with
TenantSessionMiddlewareWSGI. - CLI + SQLAlchemy — framework-agnostic
background-worker pattern with
SessionScope. - Django adopter starter — Django project template wired with TenantShield middleware + admin.
Compatibility
| Versions | |
|---|---|
| Python | 3.11, 3.12, 3.13 |
| Django | 4.2 LTS, 5.x |
| SQLAlchemy | 2.x (sync + async) |
| FastAPI | >=0.115 |
| Flask | recent versions (WSGI standard) |
structlog>=25.0,<26.0 is the only required runtime dependency.
Architectural maturity
- Phases shipped: 0 (foundation) → 1 (core API) → 2 (Django + DRF) → 3 (SQLAlchemy sync) → 4 (SQLAlchemy async + cross-adapter strategies) → 5 (production hardening).
- 557 tests (541 library + 16 example) with 99.63% library coverage.
- 12 ADRs + 43 active Decision Records + 73 normative Rules.
- 0 architectural BLOCKERs in Phase 5 (best Phase profile sustained from Phase 4).
See the Documentation for comprehensive guides, API reference, and architectural decisions.
License
Released under the Apache License 2.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tenantshield-0.7.0b0.tar.gz.
File metadata
- Download URL: tenantshield-0.7.0b0.tar.gz
- Upload date:
- Size: 356.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5db9825337b3946ecf5393fe3e32da4f925315fdf11a4a8992abeb6ed2332b30
|
|
| MD5 |
c7629ec92a58a497935d36a751faf04c
|
|
| BLAKE2b-256 |
96bc0dde8e2d42c12ce19f1eaca29560676ab3fdf981e28dfeca06442f1408b1
|
Provenance
The following attestation bundles were made for tenantshield-0.7.0b0.tar.gz:
Publisher:
publish-pypi.yml on Jhoelperaltap/tenantshield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tenantshield-0.7.0b0.tar.gz -
Subject digest:
5db9825337b3946ecf5393fe3e32da4f925315fdf11a4a8992abeb6ed2332b30 - Sigstore transparency entry: 1830366688
- Sigstore integration time:
-
Permalink:
Jhoelperaltap/tenantshield@f575faf54af0b6f11a005a1c8a3fb5288d1fcea3 -
Branch / Tag:
refs/tags/v0.7.0-beta - Owner: https://github.com/Jhoelperaltap
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f575faf54af0b6f11a005a1c8a3fb5288d1fcea3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tenantshield-0.7.0b0-py3-none-any.whl.
File metadata
- Download URL: tenantshield-0.7.0b0-py3-none-any.whl
- Upload date:
- Size: 101.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10a7f62193df78d8ebe1b96c64625ca7dcbefdfac3ee7448c5bf732c69c4ead9
|
|
| MD5 |
6a59b52e022dfd1d725ce7b32bda4bc0
|
|
| BLAKE2b-256 |
3b30a302d00195a6dbf375629649db8babd99dc7941aefecfe973bed354620cf
|
Provenance
The following attestation bundles were made for tenantshield-0.7.0b0-py3-none-any.whl:
Publisher:
publish-pypi.yml on Jhoelperaltap/tenantshield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tenantshield-0.7.0b0-py3-none-any.whl -
Subject digest:
10a7f62193df78d8ebe1b96c64625ca7dcbefdfac3ee7448c5bf732c69c4ead9 - Sigstore transparency entry: 1830366880
- Sigstore integration time:
-
Permalink:
Jhoelperaltap/tenantshield@f575faf54af0b6f11a005a1c8a3fb5288d1fcea3 -
Branch / Tag:
refs/tags/v0.7.0-beta - Owner: https://github.com/Jhoelperaltap
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f575faf54af0b6f11a005a1c8a3fb5288d1fcea3 -
Trigger Event:
push
-
Statement type: