Metadata-Version: 2.4
Name: tenantshield
Version: 0.7.0b2
Summary: Multi-tenant enforcement engine for Python. Prevents cross-tenant data leaks by construction.
Project-URL: Homepage, https://github.com/Jhoelperaltap/tenantshield
Project-URL: Documentation, https://github.com/Jhoelperaltap/tenantshield#readme
Project-URL: Repository, https://github.com/Jhoelperaltap/tenantshield
Project-URL: Bug Tracker, https://github.com/Jhoelperaltap/tenantshield/issues
Project-URL: Changelog, https://github.com/Jhoelperaltap/tenantshield/blob/main/CHANGELOG.md
Author-email: Jhoel Peralta <jhoelperaltap@gmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: django,drf,multi-tenant,saas,security,sqlalchemy,tenant-isolation
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Framework :: Django
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: structlog<26.0,>=25.0
Provides-Extra: dev
Requires-Dist: aiosqlite<1.0,>=0.22; extra == 'dev'
Requires-Dist: bandit[toml]>=1.7; extra == 'dev'
Requires-Dist: coverage[toml]>=7.6; extra == 'dev'
Requires-Dist: django-stubs[compatible-mypy]<6.0.5,>=6.0.3; extra == 'dev'
Requires-Dist: django<6.0.7,>=4.2; extra == 'dev'
Requires-Dist: djangorestframework-stubs[compatible-mypy]<4.0,>=3.16.9; extra == 'dev'
Requires-Dist: fastapi<1.0,>=0.115; extra == 'dev'
Requires-Dist: httpx<1.0,>=0.27; extra == 'dev'
Requires-Dist: hypothesis>=6.112; extra == 'dev'
Requires-Dist: mkdocs-material<10.0,>=9.5; extra == 'dev'
Requires-Dist: mkdocstrings[python]<2.0,>=1.0; extra == 'dev'
Requires-Dist: mypy<2.0,>=1.11; extra == 'dev'
Requires-Dist: pip-audit>=2.7; extra == 'dev'
Requires-Dist: pre-commit>=4.0; extra == 'dev'
Requires-Dist: pyright>=1.1.380; extra == 'dev'
Requires-Dist: pytest-asyncio<2.0,>=1.3.0; extra == 'dev'
Requires-Dist: pytest-cov<8.0,>=5.0; extra == 'dev'
Requires-Dist: pytest-django<5.0,>=4.12; extra == 'dev'
Requires-Dist: pytest<10.0,>=9.0.3; extra == 'dev'
Requires-Dist: ruff<1.0,>=0.6; extra == 'dev'
Provides-Extra: django
Requires-Dist: django<6.0.7,>=4.2; extra == 'django'
Provides-Extra: drf
Requires-Dist: djangorestframework<4.0,>=3.17.1; extra == 'drf'
Provides-Extra: jwt
Requires-Dist: pyjwt<3.0,>=2.12.1; extra == 'jwt'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# 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

```python
# 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

```python
# 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](https://jhoelperaltap.github.io/tenantshield/adapters/);
Celery integration is on the [roadmap](https://jhoelperaltap.github.io/tenantshield/adapters/#roadmap--planned),
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_aware` manager + 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_unscoped` and `auto_propagate_from_parent_fk` parity with Django
  pending; see [Known Limitations](https://jhoelperaltap.github.io/tenantshield/adapters/#known-limitations-sqlalchemy-phase-7))*:
  `@tenant_aware` declarative decorator + event-based write enforcement
  (`before_insert` / `before_update` / `before_delete`) + read filtering via
  `do_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**: `TenantSessionMiddlewareWSGI` with 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_tenant`
  for SA session binding.
- **Policy engine**: `DenyByDefaultPolicy`, `AllowListPolicy`,
  `RequireScope`, `ChainPolicy`. Composable.
- **Audit bus**: pluggable sinks (`StructLogSink`, `InMemorySink`, `NullSink`,
  or custom). 6 `AuditEventType` values (`CONTEXT_BOUND` /
  `CONTEXT_RELEASED` / `POLICY_ALLOW` / `POLICY_DENY` /
  `ENFORCEMENT_VIOLATION` / `SINK_FAILURE`).
- **Cross-adapter extraction strategies**: `HeaderStrategy`, `HostStrategy`,
  `JWTStrategy`, `CallableStrategy` operating on a minimal `RequestProtocol`
  abstraction. Same strategy instance works across Django + ASGI.

### Production hardening (Phase 5)

- **Structured observability**: `tenantshield.observability` module with a
  9-event taxonomy across scope lifecycle + enforcement + middleware boundaries.
  structlog-based emission with `~6 ns/call` disabled-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

TenantShield is published to public PyPI as a beta pre-release. Use the
`--pre` flag because `pip` does not install pre-releases by default:

```bash
pip install --pre tenantshield
```

Adapter-specific extras:

```bash
pip install --pre "tenantshield[django]"       # Django + DRF adapter
pip install --pre "tenantshield[sqlalchemy]"   # SQLAlchemy adapter
pip install --pre "tenantshield[jwt]"          # JWT strategy support
pip install --pre "tenantshield[drf]"          # Django REST Framework adapter
pip install --pre "tenantshield[all]"          # all adapters at once
```

TestPyPI continues in parallel for release-candidate testing:

```bash
pip install --pre --index-url https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple/ tenantshield
```

Once `v1.0.0` ships, `pip install tenantshield` (without `--pre`) will
prefer the stable release automatically.

## Quickstart

The canonical minimum: register an audit sink, define a policy, enter a
tenant scope, and evaluate operations.

```python
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](https://jhoelperaltap.github.io/tenantshield/adapters/)
and the [Examples](#examples) below.

### Enable observability (Phase 5, opt-in)

```python
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](https://jhoelperaltap.github.io/tenantshield/observability/quick-start/)
for adopter integration patterns.

## Documentation

- **[Getting Started](https://jhoelperaltap.github.io/tenantshield/getting-started/)** — install + minimal example.
- **[Concepts](https://jhoelperaltap.github.io/tenantshield/concepts/)** — building blocks (TenantContext,
  policies, audit bus, registry).
- **[API Reference](https://jhoelperaltap.github.io/tenantshield/api/)** — complete public surface.
- **[Adapters](https://jhoelperaltap.github.io/tenantshield/adapters/)** — Django + SQLAlchemy + middleware
  integration guides.
- **Observability**:
  - [Quick Start](https://jhoelperaltap.github.io/tenantshield/observability/quick-start/) — enable emission +
    configure structlog.
  - [Dual-Pattern](https://jhoelperaltap.github.io/tenantshield/observability/dual-pattern/) — audit bus +
    observability semantics.
  - [Async Middleware Migration](https://jhoelperaltap.github.io/tenantshield/observability/async-middleware-migration/)
  - [Production Checklist](https://jhoelperaltap.github.io/tenantshield/observability/production-checklist/)
  - [OpenTelemetry integration](https://jhoelperaltap.github.io/tenantshield/observability/integration/opentelemetry/)
  - [Prometheus integration](https://jhoelperaltap.github.io/tenantshield/observability/integration/prometheus/)
- **[Architectural Decision Records](https://github.com/Jhoelperaltap/tenantshield/tree/main/docs/adr)** — 14 ADRs documenting
  design decisions.
- **[Changelog](https://github.com/Jhoelperaltap/tenantshield/blob/main/CHANGELOG.md)** — release history with detailed Decision
  Records per phase.

## Examples

Runnable adopter examples lives in [`examples/`](https://github.com/Jhoelperaltap/tenantshield/tree/main/examples):

- **[FastAPI + SQLAlchemy](https://github.com/Jhoelperaltap/tenantshield/tree/main/examples/02_sqlalchemy/fastapi)** — async ASGI
  application with `TenantSessionMiddleware`.
- **[Flask + SQLAlchemy](https://github.com/Jhoelperaltap/tenantshield/tree/main/examples/02_sqlalchemy/flask)** — sync WSGI
  application with `TenantSessionMiddlewareWSGI`.
- **[CLI + SQLAlchemy](https://github.com/Jhoelperaltap/tenantshield/tree/main/examples/02_sqlalchemy/cli)** — framework-agnostic
  background-worker pattern with `SessionScope`.
- **[Django adopter starter](https://github.com/Jhoelperaltap/tenantshield/tree/main/examples/01_django)** — 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](https://jhoelperaltap.github.io/tenantshield/) for
comprehensive guides, API reference, and architectural decisions.

## License

Released under the [Apache License 2.0](https://github.com/Jhoelperaltap/tenantshield/blob/main/LICENSE).
