Shared multi-tenancy infrastructure for the BF Agent platform
Project description
django-tenancy
Shared multi-tenancy infrastructure for the BF Agent platform — per ADR-035.
Core Components
- Organization: Tenant model with lifecycle management (trial → active → suspended → deleted)
- Membership: User-to-organization mapping with roles (owner, admin, member, viewer, external)
- SubdomainTenantMiddleware: Resolves tenant from subdomain or
X-Tenant-IDheader - TenantAwareManager: Explicit
.for_tenant(uuid)queryset filtering - Context propagation: Async-safe contextvars + PostgreSQL RLS session variable
- Health endpoints:
/livez/(liveness) +/healthz/(readiness with DB + Redis checks) - Decorators:
tenant_context()context manager,@with_tenant_from_arg()for Celery tasks
Installation
pip install -e ".[dev]"
Usage
# settings.py
INSTALLED_APPS = [
...
"django_tenancy",
]
MIDDLEWARE = [
...
"django_tenancy.middleware.SubdomainTenantMiddleware",
]
# urls.py
from django_tenancy.healthz import liveness, readiness
urlpatterns = [
path("livez/", liveness),
path("healthz/", readiness),
]
# In views / services:
from django_tenancy.managers import TenantAwareManager
class MyModel(models.Model):
tenant_id = models.UUIDField(db_index=True)
objects = TenantAwareManager()
# Query with tenant isolation:
MyModel.objects.for_tenant(request.tenant_id)
ADR-137: TenantManager (auto-filter)
from django_tenancy.managers import TenantManager
class MyModel(models.Model):
tenant_id = models.UUIDField(db_index=True)
objects = TenantManager()
# In request context → auto-filtered by middleware tenant:
MyModel.objects.all()
# Explicit (Celery, management commands):
MyModel.objects.for_tenant(tenant_uuid)
# Admin / cross-tenant reports:
MyModel.objects.unscoped()
ADR-137: Row-Level Security (Phase 2)
Setup (once per database)
# 1. Create separate DB roles
python manage.py setup_rls_roles --dry-run # preview
python manage.py setup_rls_roles \
--app-user=risk_hub \
--app-password=<secret>
# 2. Enable RLS on all tenant tables
python manage.py enable_rls --dry-run # preview
python manage.py enable_rls # execute
# Single table:
python manage.py enable_rls --table=risk_assessment
# Remove RLS:
python manage.py enable_rls --disable
DB Role Separation
| Role | Used by | RLS |
|---|---|---|
<db>_admin (table owner) |
migrate, createsuperuser | exempt |
<db>_app (non-owner) |
gunicorn, celery | active |
After setup_rls_roles, update DATABASE_URL for
gunicorn/celery to use the app-user. Keep the admin-user
for migrations.
ADR-137: Phase 4 — RLS Rollout Checklist
Pre-requisites
- All tenant-scoped models use
TenantManager(Phase 4.1 ✅) SubdomainTenantMiddlewaresetsapp.tenant_idsession var- DB roles created via
setup_rls_roles
Rollout Steps (per environment)
# 1. Dry-run — verify SQL, no changes
python manage.py enable_rls --dry-run
# 2. Apply RLS policies
python manage.py enable_rls
# 3. Verify no query breakage (run test suite)
python -m pytest tests/ -x
# 4. Switch app to non-owner DB user
# Update DATABASE_URL in .env.prod:
# OLD: postgresql://risk_hub_admin:xxx@db/risk_hub
# NEW: postgresql://risk_hub_app:xxx@db/risk_hub
# Keep admin user for migrate service only.
# 5. Restart app containers
docker compose restart web celery
# 6. Smoke test — verify tenant isolation
curl -H "X-Tenant-ID: <uuid>" https://app/api/v1/...
Covered Tables (28 models)
| App | Models |
|---|---|
| risk | Assessment, Hazard |
| actions | ActionItem |
| documents | Document, DocumentVersion |
| approvals | ApprovalWorkflow, ApprovalRequest |
| notifications | Notification, NotificationPreference |
| permissions | Role, Scope, Assignment |
| identity | ApiKey |
| tenancy | Membership, Site |
| explosionsschutz | Area, ExplosionConcept, ZoneDefinition, ProtectionMeasure, Equipment, Inspection, ZoneIgnitionSourceAssessment, VerificationDocument, ZoneCalculationResult, EquipmentATEXCheck |
| substances | Party + all TenantScopedModel subclasses |
| gbu | HazardAssessmentActivity, ActivityMeasure |
Excluded (by design)
Organization— tenant entity itself, not tenant-scopedUser— nullable tenant_id, uses Django's UserManagerPermission,RolePermission,ApprovalStep,ApprovalDecision— no tenant_idTenantScopedMasterDatasubclasses (explosionsschutz) — nullable tenant_id, hybrid isolation
Critical Rule
Organization.id != Organization.tenant_id — always use org.tenant_id for data isolation.
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 iil_django_tenancy-0.1.0.tar.gz.
File metadata
- Download URL: iil_django_tenancy-0.1.0.tar.gz
- Upload date:
- Size: 25.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a2358ae9df559fc382b102707f84fe690f69a7ab55c59aa25d6c16703d80366
|
|
| MD5 |
7fc5514f60e1b72691bcd964a36b63e6
|
|
| BLAKE2b-256 |
e1f383c8242eec1ee8f206ee10faa799bc829c0eed8d14ef1653cea7eaf48a6b
|
File details
Details for the file iil_django_tenancy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: iil_django_tenancy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63f0745abda8f8eb0a1ae6e7c37064d5bb05773ef1f59d2120957f7ef84c76a7
|
|
| MD5 |
b6fad7b7d80476cefb52fee10d9584f5
|
|
| BLAKE2b-256 |
d104700ca313927e740f90db8f6c600e13f382e962894f2caeb35eaba502a29e
|