1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! # tenaxum
//!
//! Tenant-scoped helpers for Axum + sqlx + Postgres. Tenacious about
//! row-level isolation.
//!
//! `tenaxum` exists for one narrow job: carry a tenant identifier from
//! your Rust request/job context into Postgres so row-level security
//! policies can enforce tenant isolation at the database boundary.
//!
//! The crate has three main pieces:
//!
//! 1. [`pool`] — request-scoped pool hooks for the common Axum/sqlx path
//! 2. [`PgPoolExt`] / [`set_tenant`] — explicit transaction-scoped binding
//! for jobs, scripts, or admin paths
//! 3. [`audit`] — boot-time and CI-time checks for common RLS mistakes
//!
//! ## Minimum safe setup
//!
//! 1. Connect as a **non-superuser** Postgres role.
//! 2. Add `ENABLE ROW LEVEL SECURITY` and `FORCE ROW LEVEL SECURITY` to
//! your tenant-scoped tables.
//! 3. Use either [`pool::with_tenant_hooks`] + [`pool::tenant_scope`] or
//! [`PgPoolExt::begin_tenant`] / [`set_tenant`] on every DB path.
//! 4. Run [`audit::ensure_isolation`] at boot and fail closed if it
//! returns findings.
//!
//! ## Quick start
//!
//! ```no_run
//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
//! use axum::{middleware, Router};
//! use sqlx::postgres::PgPoolOptions;
//! use tenaxum::{audit, pool};
//!
//! let pool = pool::with_tenant_hooks(PgPoolOptions::new().max_connections(8))
//! .connect("postgres://...").await?;
//!
//! let report = audit::ensure_isolation(&pool).await?;
//! if !report.is_clean() {
//! panic!("RLS invariants broken at boot:\n{report}");
//! }
//!
//! let app = Router::new()
//! .route("/", axum::routing::get(|| async { "ok" }))
//! .layer(middleware::from_fn(pool::tenant_scope))
//! .with_state(pool);
//! # let _ = app;
//! # Ok(()) }
//! ```
//!
//! Your auth layer inserts `TenantId` into request extensions:
//! `req.extensions_mut().insert(TenantId::from(...))`. The middleware
//! scopes that value for the async call chain; the pool hooks read it and
//! set the configured Postgres GUC.
//!
//! ## What you still provide
//!
//! `tenaxum` removes the repetitive tenant-plumbing around sqlx and
//! Postgres RLS, but it still assumes your app supplies four things:
//!
//! - **Authentication and tenant resolution.** Your auth/session layer
//! must decide which tenant the caller belongs to.
//! - **`TenantId` insertion.** For request-scoped usage, your middleware
//! inserts `TenantId` into request extensions before
//! [`pool::tenant_scope`] runs.
//! - **RLS policy SQL.** Your migrations still define the actual
//! `USING` / `WITH CHECK` predicates.
//! - **Non-request wiring.** Jobs, scripts, queue consumers, and spawned
//! tasks still need [`PgPoolExt::begin_tenant`], [`set_tenant`], or
//! [`pool::spawn_with_tenant`] on the DB paths that should be scoped.
//!
//! ## Adoption checklist
//!
//! If you want the "install it and stop thinking about tenant plumbing"
//! path, this is the checklist:
//!
//! 1. Wrap your pool builder with [`pool::with_tenant_hooks`].
//! 2. Insert [`TenantId`] only after auth has resolved the correct
//! tenant for the caller.
//! 3. Add [`pool::tenant_scope`] to the request path before handlers that
//! touch tenant-scoped data.
//! 4. Use [`pool::spawn_with_tenant`] for spawned child tasks, and
//! [`PgPoolExt::begin_tenant`] / [`set_tenant`] for jobs and scripts.
//! 5. Add `ENABLE ROW LEVEL SECURITY`, `FORCE ROW LEVEL SECURITY`, and a
//! correct tenant policy to every tenant-scoped table.
//! 6. Connect as a **non-superuser** role in every environment.
//! 7. Run [`audit::ensure_isolation`] at boot and fail closed on findings.
//! 8. Keep the smoke path passing: `cargo test -p tenaxum --test smoke`.
//!
//! ## Async model
//!
//! Tenant binding is stored in a Tokio task-local. That means it flows
//! through ordinary async calls, but **does not automatically cross**
//! [`tokio::spawn`] boundaries. For spawned child tasks, use
//! [`pool::spawn_with_tenant`] or manually wrap the future with
//! [`pool::scope_tenant`].
//!
//! ```no_run
//! # async fn run(pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
//! use axum::extract::State;
//! use tenaxum::pool;
//!
//! async fn fan_out(State(pool): State<sqlx::PgPool>) -> sqlx::Result<()> {
//! let child_pool = pool.clone();
//! pool::spawn_with_tenant(async move {
//! sqlx::query("SELECT 1").execute(&child_pool).await
//! })
//! .await??;
//! Ok(())
//! }
//! # let _ = fan_out;
//! # Ok(()) }
//! ```
//!
//! ## Audit model
//!
//! [`audit::ensure_isolation`] is the real safety check: it inspects the
//! live schema and reports common tenant-isolation mistakes.
//!
//! [`audit::scan_migrations`] is intentionally weaker. It is a
//! lightweight CI lint for obvious `CREATE POLICY ...` omissions, not a
//! full SQL parser and not a security guarantee.
//!
//! ## Failure modes and mitigations
//!
//! The common places an app can still fail are:
//!
//! - **Wrong tenant resolved by auth.**
//! Mitigation: keep tenant resolution in one place, test it directly,
//! and only insert [`TenantId`] after auth has verified membership.
//! - **A DB path bypasses the integration.**
//! Mitigation: wrap the pool once at construction time, use
//! [`pool::tenant_scope`] consistently, and treat jobs/spawned tasks as
//! first-class integration points rather than exceptions.
//! - **Broken RLS policy or deployment config.**
//! Mitigation: use `FORCE`, avoid superuser roles, and run
//! [`audit::ensure_isolation`] on boot.
//! - **Side systems ignore the same contract.**
//! Mitigation: make workers, scripts, and maintenance paths use the
//! same non-superuser role and the same tenant-binding helpers.
//!
//! ## What tenaxum cannot guarantee
//!
//! - It does **not** prove your RLS predicates are semantically correct.
//! A syntactically valid policy can still be wrong.
//! - It does **not** make superuser connections safe. Postgres superusers
//! bypass RLS unconditionally.
//! - It does **not** make [`audit::scan_migrations`] equivalent to a
//! live-schema audit.
//! - It does **not** authenticate users or derive tenant identity for
//! you. It assumes the [`TenantId`] you provide is already correct.
//!
//! ## Configuration
//!
//! Every assumption is configurable through [`Tenancy`] — the GUC name
//! (`app.tenant_id`), schema list (`public`), and tenant-column name
//! (`tenant_id`) all default to common values, and any can be overridden.
//! [`Tenancy::default`] reproduces the v0.1 behaviour.
//!
//! ## Pattern 1 — pool-scoped (recommended for production apps)
//!
//! Set the GUC once when a connection is checked out of the pool, reset
//! it on release. Every query in the scoped async call chain is
//! auto-isolated. Zero per-call-site boilerplate.
//!
//! See [`pool`] for the [`pool::with_tenant_hooks`] free fn (default
//! config) and [`Tenancy::with_tenant_hooks`] for the configured form, plus
//! the [`pool::tenant_scope`] Axum middleware.
//!
//! ## Pattern 2 — explicit `begin_tenant`
//!
//! Open a transaction and `SET LOCAL` the GUC inside it. Useful for
//! background jobs, one-off scripts, or admin paths where the pool hooks
//! aren't wired.
//!
//! See [`PgPoolExt::begin_tenant`] (default config), [`Tenancy::begin_tenant`]
//! (configured), and [`set_tenant`] / [`Tenancy::set_tenant`].
//!
//! ## Pattern 3 — boot-time invariant audit
//!
//! Refuse to start the app on a broken schema. See [`audit::ensure_isolation`]
//! / [`Tenancy::ensure_isolation`] and [`audit::scan_migrations`] /
//! [`Tenancy::scan_migrations`].
//!
//! ## What tenaxum deliberately does not do
//!
//! - **JWT decoding.** Every app does this differently. Decode in your own
//! middleware, then `req.extensions_mut().insert(TenantId::from(...))`.
//! - **RLS policy generation.** The policy is one line of SQL; see the
//! `examples/rls` crate in this repo for the full pattern, including
//! the `FORCE ROW LEVEL SECURITY` + non-superuser-role + `WITH CHECK`
//! gotchas.
//! - **Scope/permission middleware.** Maybe later, once the design has
//! been battle-tested in a real codebase.
pub use Tenancy;
pub use TenantId;
pub use ;