- Removed the `/api/notifications/stream/` endpoint and associated SSE logic. - Updated `Bell.svelte` and `notifications.svelte.js` to implement polling for notifications every 45 seconds. - Introduced connection pooling configuration in environment variables documentation. - Updated production deployment documentation to recommend WSGI over ASGI for improved database connection management.
62 lines
3.2 KiB
Python
62 lines
3.2 KiB
Python
"""Connection-pool hooks that keep RLS context from leaking between tenants.
|
|
|
|
**Why this module exists.** `app.current_org` is set with
|
|
``set_config(<var>, <org>, false)``, i.e. **SESSION** scope. That is deliberate:
|
|
Django runs in autocommit, so a transaction-local (`true`) setting would vanish
|
|
before the next statement in the same request. The cost is that the value
|
|
outlives the statement, the transaction, and the request. It is cleared only
|
|
because something explicitly clears it.
|
|
|
|
Without pooling that was survivable: ``CONN_MAX_AGE=0`` closed the backend at
|
|
the end of every request, taking the session state with it. With a pool, the
|
|
same backend is handed to the next borrower still carrying the previous
|
|
tenant's org id, and every RLS policy on the database happily scopes that
|
|
borrower's queries to the wrong tenant. psycopg_pool does **not** clean session
|
|
state on its own: ``ConnectionPool._reset_connection`` only rolls back an open
|
|
transaction. A `SET`/`set_config` survives a rollback when it was made at
|
|
session scope outside that transaction.
|
|
|
|
So the reset below is not a tidiness measure. It is the thing standing between
|
|
a pooled deployment and a cross-tenant read.
|
|
|
|
**This module must not import Django.** ``crm/settings.py`` imports it while
|
|
building ``DATABASES``, long before the app registry is ready. It takes a raw
|
|
psycopg connection and speaks SQL.
|
|
|
|
IMPORTANT: this module cannot be exercised by the default test suite, which
|
|
runs on SQLite (``crm/test_settings.py``) and has no ``set_config`` at all.
|
|
See ``common/tests/test_pool_rls_isolation.py``, which is marked
|
|
``postgres_only`` and needs a non-superuser role to mean anything, since a
|
|
superuser bypasses RLS entirely.
|
|
"""
|
|
|
|
from . import CONTEXT_VARIABLE
|
|
|
|
# Set to the empty string rather than RESET/DISCARD. The isolation policies
|
|
# generated by `get_enable_policy_sql` compare against
|
|
# `NULLIF(current_setting(<var>, true), '')`, so empty and unset both fail
|
|
# closed to zero rows. Assigning '' keeps the GUC defined, which means
|
|
# `current_setting(<var>, true)` never raises for later readers.
|
|
CLEAR_CONTEXT_SQL = f"SELECT set_config('{CONTEXT_VARIABLE}', '', false)"
|
|
|
|
|
|
def reset_rls_context(conn):
|
|
"""psycopg_pool ``reset`` callback: clear tenant context before reuse.
|
|
|
|
Contract, from ``ConnectionPool._reset_connection``: if this function
|
|
raises, or leaves the connection in any transaction status other than
|
|
IDLE, the pool logs a warning and **discards the connection**. A reset that
|
|
silently dirties every connection turns the pool into an expensive way of
|
|
opening one connection per request, which looks like pooling doing nothing
|
|
rather than like an error. Hence the explicit commit below.
|
|
|
|
The connection arrives IDLE (the pool rolls back an open transaction before
|
|
calling us). Under autocommit the statement stands alone and we stay IDLE.
|
|
Without autocommit the statement opens a transaction, so we must commit to
|
|
return to IDLE. Commit, not rollback: a session-scoped `set_config` made
|
|
inside a transaction is *reverted* by a rollback, which would leave the
|
|
stale org id in place and defeat the entire point.
|
|
"""
|
|
conn.execute(CLEAR_CONTEXT_SQL)
|
|
if not conn.autocommit:
|
|
conn.commit()
|