API Reference¶
Auto-generated from source code docstrings.
Top-Level Exports¶
The most common symbols are available directly from django_rls_tenants:
from django_rls_tenants import (
AddM2MRLSPolicy,
RLSConstraint,
RLSM2MConstraint,
RLSManager,
RLSProtectedModel,
RLSTenantMiddleware,
RLSTenantModelAdmin,
TenantQuerySet,
TenantUser,
admin_context,
current_tenant_value_sql,
safe_tenant_sql,
tenant_context,
with_rls_context,
)
Removed from top-level in v1.2.1
Raw state functions (get_current_tenant_id, set_current_tenant_id,
reset_current_tenant_id, get_rls_context_active, set_rls_context_active,
reset_rls_context_active) and exception classes (NoTenantContextError,
RLSConfigurationError, RLSTenantError) are no longer re-exported from the
top-level package. Import them from their actual modules instead:
Exceptions¶
Custom exception hierarchy for precise error handling. All exceptions live in
django_rls_tenants.exceptions. Import them from that module directly.
django_rls_tenants.exceptions.RLSTenantError
¶
Bases: Exception
Base exception for all django-rls-tenants errors.
Catch this to handle any error raised by the library.
Accepts an optional, keyword-only hint describing how to fix the
error. When supplied, it is appended to str(exc) after a blank line
and a Hint: label, so the suggestion shows up in tracebacks and
logs. The bare message and the hint are also exposed as
attributes for programmatic access.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
Human-readable description of what went wrong. |
required |
hint
|
str | None
|
Optional actionable suggestion for fixing the error. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
message |
The error description, without the hint. |
|
hint |
The actionable suggestion, or |
Source code in django_rls_tenants/exceptions.py
django_rls_tenants.exceptions.NoTenantContextError
¶
Bases: RLSTenantError
Query or context operation attempted without an active tenant context.
Raised when STRICT_MODE=True and a queryset evaluation is attempted
without an active tenant_context(), admin_context(),
for_user(), or RLSTenantMiddleware context.
Also raised by tenant_context() and _resolve_user_guc_vars()
when a non-admin user has rls_tenant_id=None.
Source code in django_rls_tenants/exceptions.py
django_rls_tenants.exceptions.RLSConfigurationError
¶
Bases: RLSTenantError
Invalid or missing RLS configuration.
Raised when a required configuration key is missing from
settings.RLS_TENANTS or when a configuration value is invalid.
Source code in django_rls_tenants/exceptions.py
RLS Layer¶
Generic PostgreSQL Row-Level Security primitives. This layer has zero imports
from tenants/.
GUC Helpers¶
django_rls_tenants.rls.guc.set_guc
¶
Set a PostgreSQL session variable (GUC).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Variable name (e.g., |
required |
value
|
str
|
Variable value as string. |
required |
is_local
|
bool
|
If |
False
|
using
|
str
|
Database alias. Default: |
'default'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Source code in django_rls_tenants/rls/guc.py
django_rls_tenants.rls.guc.get_guc
¶
Get a PostgreSQL session variable value.
Returns:
| Type | Description |
|---|---|
str | None
|
The variable value, or |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in django_rls_tenants/rls/guc.py
django_rls_tenants.rls.guc.clear_guc
¶
Clear a GUC variable by setting it to an empty string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Variable name to clear. |
required |
is_local
|
bool
|
If |
False
|
using
|
str
|
Database alias. Default: |
'default'
|
Source code in django_rls_tenants/rls/guc.py
Constraints¶
django_rls_tenants.rls.constraints.RLSConstraint
¶
RLSConstraint(
*,
field: str,
name: str,
guc_tenant_var: str = "rls.current_tenant",
guc_admin_var: str = "rls.is_admin",
tenant_pk_type: str = "int",
extra_bypass_flags: list[str] | None = None,
)
Bases: BaseConstraint
Django constraint that generates PostgreSQL RLS policies during migrations.
When Django applies a migration containing this constraint, it:
- Enables RLS on the table (
ALTER TABLE ... ENABLE ROW LEVEL SECURITY). - Forces RLS for the table owner (
ALTER TABLE ... FORCE ROW LEVEL SECURITY). - Creates an isolation policy with configurable
USINGandWITH CHECK.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
field
|
str
|
FK field name for tenant identification (e.g., |
required |
name
|
str
|
Constraint name. Supports |
required |
guc_tenant_var
|
str
|
GUC variable holding the current tenant ID.
Default: |
'rls.current_tenant'
|
guc_admin_var
|
str
|
GUC variable for admin bypass.
Default: |
'rls.is_admin'
|
tenant_pk_type
|
str
|
SQL cast type for tenant PK.
Default: |
'int'
|
extra_bypass_flags
|
list[str] | None
|
Additional GUC variables that bypass the |
None
|
Source code in django_rls_tenants/rls/constraints.py
constraint_sql
¶
No inline constraint SQL; defer RLS DDL to after CREATE TABLE.
Django calls constraint_sql during CREATE TABLE for inline
constraints. RLS policies require the table to exist first, so we
defer the actual DDL and return an empty string (filtered out by
Django's if statement guard).
Source code in django_rls_tenants/rls/constraints.py
create_sql
¶
create_sql(
model: type[Model] | None,
schema_editor: BaseDatabaseSchemaEditor | None,
) -> Statement
Generate SQL to enable RLS and create the isolation policy.
Source code in django_rls_tenants/rls/constraints.py
remove_sql
¶
remove_sql(
model: type[Model] | None,
schema_editor: BaseDatabaseSchemaEditor | None,
) -> Statement
Generate SQL to drop the policy and disable RLS.
Source code in django_rls_tenants/rls/constraints.py
validate
¶
validate(
model: type[Model],
instance: Any,
exclude: Any = None,
using: str | None = None,
) -> None
No-op: RLS is enforced at the database level, not in Django validation.
deconstruct
¶
Return a 3-tuple for Django's migration serializer.
Source code in django_rls_tenants/rls/constraints.py
django_rls_tenants.rls.constraints.RLSM2MConstraint
¶
RLSM2MConstraint(
*,
name: str,
from_model: str,
to_model: str,
from_fk: str,
to_fk: str,
from_tenant_fk: str | None = "tenant",
to_tenant_fk: str | None = "tenant",
guc_tenant_var: str = "rls.current_tenant",
guc_admin_var: str = "rls.is_admin",
tenant_pk_type: str = "int",
)
Bases: BaseConstraint
Constraint generating subquery-based RLS policies for M2M join tables.
Unlike RLSConstraint which checks a direct {field}_id column,
this generates policies that verify both FK references point to rows
belonging to the current tenant via IN (SELECT ...) subqueries.
For join tables where only one side is RLS-protected, only that side's FK is checked. For both sides protected, both FKs are checked.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Constraint name. |
required |
from_model
|
str
|
Dotted path to the "from" model (e.g., |
required |
to_model
|
str
|
Dotted path to the "to" model (e.g., |
required |
from_fk
|
str
|
FK column name for the "from" side (e.g., |
required |
to_fk
|
str
|
FK column name for the "to" side (e.g., |
required |
from_tenant_fk
|
str | None
|
Tenant FK field name on the "from" model,
or |
'tenant'
|
to_tenant_fk
|
str | None
|
Tenant FK field name on the "to" model,
or |
'tenant'
|
guc_tenant_var
|
str
|
GUC variable for current tenant.
Default: |
'rls.current_tenant'
|
guc_admin_var
|
str
|
GUC variable for admin bypass.
Default: |
'rls.is_admin'
|
tenant_pk_type
|
str
|
SQL cast type for tenant PK.
Default: |
'int'
|
Source code in django_rls_tenants/rls/constraints.py
constraint_sql
¶
No inline constraint SQL; defer RLS DDL to after CREATE TABLE.
Source code in django_rls_tenants/rls/constraints.py
create_sql
¶
create_sql(
model: type[Model] | None,
schema_editor: BaseDatabaseSchemaEditor | None,
) -> Statement
Generate SQL to enable RLS and create the M2M isolation policy.
Source code in django_rls_tenants/rls/constraints.py
remove_sql
¶
remove_sql(
model: type[Model] | None,
schema_editor: BaseDatabaseSchemaEditor | None,
) -> Statement
Generate SQL to drop the M2M policy and disable RLS.
Source code in django_rls_tenants/rls/constraints.py
validate
¶
validate(
model: type[Model],
instance: Any,
exclude: Any = None,
using: str | None = None,
) -> None
No-op: RLS is enforced at the database level, not in Django validation.
deconstruct
¶
Return a 3-tuple for Django's migration serializer.
Source code in django_rls_tenants/rls/constraints.py
Migration Operations¶
django_rls_tenants.operations.AddM2MRLSPolicy
¶
AddM2MRLSPolicy(
m2m_table: str,
from_model: str,
to_model: str,
from_fk: str,
to_fk: str,
from_tenant_fk: str | None = "tenant",
to_tenant_fk: str | None = "tenant",
guc_tenant_var: str = "rls.current_tenant",
guc_admin_var: str = "rls.is_admin",
tenant_pk_type: str = "int",
)
Bases: Operation
Migration operation to add an RLS policy to an M2M through table.
Generates and executes subquery-based CREATE POLICY SQL that
checks both FK sides of the M2M join table belong to the current
tenant. Supports tables where only one side is RLS-protected.
This operation is reversible: database_backwards drops the policy
and disables RLS on the table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
m2m_table
|
str
|
The database table name of the M2M through table. |
required |
from_model
|
str
|
Dotted path to the "from" model (e.g., |
required |
to_model
|
str
|
Dotted path to the "to" model (e.g., |
required |
from_fk
|
str
|
FK column on the through table for the "from" side. |
required |
to_fk
|
str
|
FK column on the through table for the "to" side. |
required |
from_tenant_fk
|
str | None
|
Tenant FK on the "from" model, or |
'tenant'
|
to_tenant_fk
|
str | None
|
Tenant FK on the "to" model, or |
'tenant'
|
guc_tenant_var
|
str
|
GUC variable for current tenant.
Default: |
'rls.current_tenant'
|
guc_admin_var
|
str
|
GUC variable for admin bypass.
Default: |
'rls.is_admin'
|
tenant_pk_type
|
str
|
SQL cast type for tenant PK.
Default: |
'int'
|
Source code in django_rls_tenants/operations.py
state_forwards
¶
database_forwards
¶
database_forwards(
app_label: str,
schema_editor: BaseDatabaseSchemaEditor,
from_state: ProjectState,
to_state: ProjectState,
) -> None
Create the M2M RLS policy.
Source code in django_rls_tenants/operations.py
database_backwards
¶
database_backwards(
app_label: str,
schema_editor: BaseDatabaseSchemaEditor,
from_state: ProjectState,
to_state: ProjectState,
) -> None
Drop the M2M RLS policy and disable RLS.
Source code in django_rls_tenants/operations.py
describe
¶
deconstruct
¶
Return args for Django's migration serializer.
Source code in django_rls_tenants/operations.py
Context Managers¶
django_rls_tenants.rls.context.rls_context
¶
rls_context(
variables: dict[str, str],
*,
is_local: bool = False,
using: str = "default",
) -> Iterator[None]
Set multiple GUC variables for the duration of a block.
Saves and restores previous values on exit (supports nesting).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
variables
|
dict[str, str]
|
Dict of GUC variable names to values. |
required |
is_local
|
bool
|
If |
False
|
using
|
str
|
Database alias. Default: |
'default'
|
Source code in django_rls_tenants/rls/context.py
django_rls_tenants.rls.context.bypass_flag
¶
Temporarily set a GUC bypass flag to 'true'.
Saves and restores previous value on exit (supports nesting).
Usage::
with bypass_flag("rls.is_login_request"):
user = User.objects.get(email=email)
Source code in django_rls_tenants/rls/context.py
Tenants Layer¶
Django multitenancy built on top of the rls/ primitives.
Configuration¶
django_rls_tenants.tenants.conf.RLSTenantsConfig
¶
Read library configuration from settings.RLS_TENANTS.
All settings live under a single dict::
RLS_TENANTS = {
"TENANT_MODEL": "myapp.Tenant", # Required
"DATABASES": ["default"], # Default: ["default"]
"GUC_PREFIX": "rls", # Default: "rls"
"STRICT_MODE": False, # Default: False
"TENANT_FK_FIELD": "tenant", # Default: "tenant"
"USER_PARAM_NAME": "as_user", # Default: "as_user"
"TENANT_PK_TYPE": "int", # Default: "int"
"USE_LOCAL_SET": False, # Default: False
}
Source code in django_rls_tenants/tenants/conf.py
TENANT_FK_FIELD
property
¶
FK field name on RLSProtectedModel. Default: "tenant".
USER_PARAM_NAME
property
¶
Parameter name @with_rls_context looks for. Default: "as_user".
STRICT_MODE
property
¶
Raise on queries without tenant context. Default: False.
When enabled, TenantQuerySet evaluation methods raise
NoTenantContextError if no RLS context is active (no
tenant_context(), admin_context(), for_user(),
or RLSTenantMiddleware).
DATABASES
property
¶
Database aliases to set GUCs on. Default: ["default"].
In multi-database setups (e.g., read replicas), add all aliases that serve RLS-protected queries::
RLS_TENANTS = {
"DATABASES": ["default", "replica"],
}
Models¶
django_rls_tenants.tenants.models.RLSProtectedModel
¶
Bases: Model
Abstract base model for tenant-scoped models.
Provides:
- A
tenantForeignKey added dynamically via theclass_preparedsignal (target read fromRLS_TENANTS["TENANT_MODEL"]). RLSManageras the default manager (withfor_user()).RLSConstraintinMeta.constraints(generates RLS policy).
Usage::
class Order(RLSProtectedModel):
product = models.CharField(max_length=255)
amount = models.DecimalField(...)
class Meta(RLSProtectedModel.Meta):
db_table = "order"
To customize the tenant FK (e.g., nullable for admin users),
declare the field directly on your model -- the class_prepared
handler will not add a duplicate::
class User(AbstractUser, RLSProtectedModel):
tenant = models.ForeignKey(
Tenant, on_delete=models.CASCADE,
null=True, blank=True,
)
Managers¶
django_rls_tenants.tenants.managers.TenantQuerySet
¶
Bases: QuerySet
QuerySet that sets RLS GUC variables at evaluation time.
Stores the user reference from for_user() and defers GUC setup
to _fetch_all(), ensuring lazy querysets work correctly with RLS.
When auto-scope is active (via tenant_context() or middleware),
select_related() automatically adds WHERE related.tenant_id = X
for joined RLS-protected tables, enabling index usage on both sides
of the join.
.. warning:: Limitation of for_user() GUC management
GUC variables are only set during ``_fetch_all()`` (iteration).
QuerySet methods that bypass ``_fetch_all()`` — such as
``count()``, ``exists()``, ``aggregate()``, ``update()``,
``delete()``, and ``iterator()`` — will **not** have GUC
variables set by ``for_user()``.
For non-admin users this is safe because ``for_user()`` also
adds a Django ORM ``WHERE tenant_id = X`` filter. For **admin
users** (``is_tenant_admin=True``), no ORM filter is applied,
so these methods run against whatever GUC state the connection
already has.
For non-middleware contexts (Celery tasks, management commands),
use ``tenant_context()`` or ``admin_context()`` instead, which
set GUCs at the connection level for the entire block.
Source code in django_rls_tenants/tenants/managers.py
for_user
¶
Scope this queryset to the given user's tenant.
For admin users: returns all rows (RLS admin bypass at eval time). For tenant users: returns rows matching the user's tenant.
The queryset remains lazy and chainable. GUC variables are set
when the queryset is evaluated, not when for_user() is called.
.. warning::
GUC variables are only set during iteration (``_fetch_all``).
Methods like ``count()``, ``exists()``, ``aggregate()``,
``update()``, ``delete()``, and ``iterator()`` bypass
``_fetch_all`` and will **not** have GUC variables set.
For admin users this means those methods run without GUC
protection. Use ``tenant_context()`` or ``admin_context()``
for full coverage in non-middleware contexts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
as_user
|
TenantUser
|
User object satisfying the |
required |
Source code in django_rls_tenants/tenants/managers.py
select_related
¶
Override to add tenant filters on joined RLS-protected tables.
When a tenant context is active (or for_user() was called),
adds WHERE related.tenant_id = X for each explicitly named
relation that targets an RLS-protected model. This enables
PostgreSQL to use composite indexes on joined tables instead of
relying solely on per-row RLS current_setting() evaluation.
Falls back to super().select_related() when no tenant scope
is active or when called with no arguments (select-all mode).
Handles select_related(False) (Django 5.x) and
select_related(None) (Django 6.0+) for clearing without
adding tenant filters.
Source code in django_rls_tenants/tenants/managers.py
count
¶
exists
¶
aggregate
¶
update
¶
delete
¶
iterator
¶
bulk_create
¶
bulk_create(
objs: Any,
batch_size: int | None = None,
ignore_conflicts: bool = False,
update_conflicts: bool = False,
update_fields: Any = None,
unique_fields: Any = None,
) -> list[Any]
Guard bulk_create() with strict mode check.
Source code in django_rls_tenants/tenants/managers.py
bulk_update
¶
Guard bulk_update() with strict mode check.
Source code in django_rls_tenants/tenants/managers.py
get
¶
first
¶
django_rls_tenants.tenants.managers.RLSManager
¶
Bases: Manager
Manager for RLS-protected models.
Provides for_user() for scoped queries and
prepare_tenant_in_model_data() for resolving tenant FKs.
get_queryset
¶
Return a TenantQuerySet instance, auto-scoped if a tenant context is active.
When tenant_context(), admin_context(), or RLSTenantMiddleware
has set a current tenant ID, the queryset is automatically filtered by
WHERE tenant_id = X. This enables PostgreSQL to use composite indexes
instead of relying solely on RLS current_setting() calls.
Source code in django_rls_tenants/tenants/managers.py
for_user
¶
prepare_tenant_in_model_data
¶
Resolve a raw tenant ID for model creation.
If model_data contains a raw tenant ID (int/str) under
the configured FK field name, sets the FK column directly
({field}_id) to avoid a SELECT query. Allows passing
tenant=42 in creation data without N+1 overhead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_data
|
dict[str, Any]
|
Dict of field names to values. |
required |
as_user
|
TenantUser
|
User for context (unused here but part of API). |
required |
Source code in django_rls_tenants/tenants/managers.py
Context Managers¶
django_rls_tenants.tenants.context.tenant_context
¶
Set RLS context to a specific tenant. Supports nesting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
int | str
|
The tenant PK to scope queries to. |
required |
using
|
str
|
Database alias. Default: |
'default'
|
Raises:
| Type | Description |
|---|---|
NoTenantContextError
|
If |
Source code in django_rls_tenants/tenants/context.py
django_rls_tenants.tenants.context.admin_context
¶
Set RLS context to admin mode. Supports nesting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
using
|
str
|
Database alias. Default: |
'default'
|
Source code in django_rls_tenants/tenants/context.py
django_rls_tenants.tenants.context.with_rls_context
¶
with_rls_context(
func: Callable[_P, _R] | None = None,
*,
user_param: str | None = None,
) -> (
Callable[_P, _R]
| Callable[[Callable[_P, _R]], Callable[_P, _R]]
)
Decorator that extracts a user argument and sets RLS context.
Can be used bare or with an explicit user_param::
@with_rls_context
def my_view(request, as_user): ...
@with_rls_context(user_param="current_user")
def my_view(request, current_user): ...
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
func
|
Callable[_P, _R] | None
|
The function to decorate (when used without parentheses). |
None
|
user_param
|
str | None
|
Override the parameter name to look for. Defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
Callable[_P, _R] | Callable[[Callable[_P, _R]], Callable[_P, _R]]
|
The decorated function with the same signature as the original, |
Callable[_P, _R] | Callable[[Callable[_P, _R]], Callable[_P, _R]]
|
or a decorator factory when called with keyword arguments. |
When the user argument is None, logs a warning and proceeds
without context (fail-closed: RLS blocks all access).
Source code in django_rls_tenants/tenants/context.py
Raw SQL¶
Helpers for scoping hand-written SQL to the current tenant. See the Raw SQL guide for usage and safety notes.
django_rls_tenants.tenants.sql.safe_tenant_sql
¶
safe_tenant_sql(
column: str = "tenant_id",
*,
table: str | None = None,
include_admin: bool = True,
extra_bypass_flags: list[str] | None = None,
) -> str
Return a WHERE-clause fragment scoping rows to the current tenant.
The fragment compares column against the current-tenant GUC, using the
exact expression the RLS policies use (via
:mod:~django_rls_tenants.rls.policy_sql). When include_admin is true
it also lets rows through while the admin-bypass GUC is set, mirroring
admin_context(). Splice it straight into a raw query::
sql = f"SELECT * FROM orders WHERE {safe_tenant_sql()} AND amount > %s"
cursor.execute(sql, [100])
There are deliberately no bind parameters: the tenant id is read inside PostgreSQL from the session GUC, so the fragment contains no Python-side user input.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column
|
str
|
Tenant foreign-key column on the target table. Defaults to
|
'tenant_id'
|
table
|
str | None
|
Optional table name/alias to qualify the column with, e.g.
|
None
|
include_admin
|
bool
|
When |
True
|
extra_bypass_flags
|
list[str] | None
|
Additional boolean bypass GUCs that should also let
rows through, matching the |
None
|
Returns:
| Type | Description |
|---|---|
str
|
A SQL |
str
|
wrapped in parentheses ( |
str
|
composes safely with surrounding |
str
|
|
Warning
The parentheses only make the fragment safe to combine with AND.
Appending OR defeats tenant isolation -- WHERE {safe_tenant_sql()}
OR is_public returns rows from every tenant. Always restrict
further with AND, never OR.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Example
safe_tenant_sql("tenant_id", include_admin=False) "tenant_id = nullif((SELECT current_setting('rls.current_tenant', true)), '')::int"
The default (include_admin=True) wraps that predicate as
(<predicate> OR (SELECT current_setting('rls.is_admin', true)) = 'true').
Source code in django_rls_tenants/tenants/sql.py
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 | |
django_rls_tenants.tenants.sql.current_tenant_value_sql
¶
Return the current-tenant value expression for use in raw SQL.
Produces the same cast GUC read the RLS policies compare against -- e.g.
nullif((SELECT current_setting('rls.current_tenant', true)), '')::int --
suitable for an INSERT value list or a SELECT projection::
cursor.execute(
f"INSERT INTO orders (product, tenant_id) VALUES (%s, {current_tenant_value_sql()})",
["Widget"],
)
An unset GUC evaluates to NULL (via nullif(..., '')), so the cast
never fails on an empty string.
Returns:
| Type | Description |
|---|---|
str
|
A SQL value expression yielding the current tenant id (or |
str
|
no tenant context is active). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the configured GUC name or tenant PK type is invalid. |
Example
current_tenant_value_sql() "nullif((SELECT current_setting('rls.current_tenant', true)), '')::int"
Source code in django_rls_tenants/tenants/sql.py
State¶
Internal helpers
State functions are internal helpers for custom middleware and advanced use cases.
Prefer tenant_context() and admin_context() for managing RLS state.
Import state functions from django_rls_tenants.tenants.state, not the top-level package.
Tenant ID¶
django_rls_tenants.tenants.state.get_current_tenant_id
¶
Return the current tenant ID, or None if no tenant context is active.
Returns:
| Type | Description |
|---|---|
int | str | None
|
The tenant ID set by the innermost active |
int | str | None
|
middleware, or |
Source code in django_rls_tenants/tenants/state.py
django_rls_tenants.tenants.state.set_current_tenant_id
¶
Set the current tenant ID for automatic query scoping.
Returns a token that can be passed to reset_current_tenant_id()
to restore the previous value (for nesting support).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
int | str | None
|
The tenant PK, or |
required |
Returns:
| Type | Description |
|---|---|
Token[int | str | None]
|
A |
Source code in django_rls_tenants/tenants/state.py
django_rls_tenants.tenants.state.reset_current_tenant_id
¶
Restore the previous tenant ID using a token from set_current_tenant_id().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
Token[int | str | None]
|
The token returned by the corresponding |
required |
Source code in django_rls_tenants/tenants/state.py
RLS Context Active (Strict Mode)¶
django_rls_tenants.tenants.state.get_rls_context_active
¶
Return whether an RLS context is currently active.
An RLS context is active when tenant_context(),
admin_context(), or RLSTenantMiddleware has established
a context for the current execution scope. Used by strict mode
to distinguish "no context" from "admin context".
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in django_rls_tenants/tenants/state.py
django_rls_tenants.tenants.state.set_rls_context_active
¶
Set the RLS context active flag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
active
|
bool
|
Whether an RLS context is active. |
required |
Returns:
| Type | Description |
|---|---|
Token[bool]
|
A |
Source code in django_rls_tenants/tenants/state.py
django_rls_tenants.tenants.state.reset_rls_context_active
¶
Restore the previous RLS context active state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
Token[bool]
|
The token returned by |
required |
Middleware¶
django_rls_tenants.tenants.middleware.RLSTenantMiddleware
¶
Bases: MiddlewareMixin
Set RLS context for each authenticated request.
For authenticated users: sets tenant_context or admin_context
based on the user's TenantUser protocol implementation.
For unauthenticated requests: no context is set. RLS policies block all access to protected tables (fail-closed).
This is API-agnostic -- works identically for REST, GraphQL, Django views, or any other request handler.
Add to MIDDLEWARE::
MIDDLEWARE = [
...
"django_rls_tenants.tenants.middleware.RLSTenantMiddleware",
]
process_request
¶
Set GUC variables on all configured databases.
Iterates over RLS_TENANTS["DATABASES"] and sets GUCs on each
alias. If setting GUCs on one database fails, clears all GUCs on
databases that were already set, then re-raises.
Source code in django_rls_tenants/tenants/middleware.py
process_response
¶
Clear GUC variables and auto-scope state to prevent cross-request leaks.
Source code in django_rls_tenants/tenants/middleware.py
process_exception
¶
Clear RLS state on unhandled exceptions to prevent ContextVar leaks.
Without this, a view exception that prevents process_response
from running would leave the ContextVar set for the remainder of
the thread (WSGI) or async task (ASGI).
Source code in django_rls_tenants/tenants/middleware.py
Admin¶
Tenant-aware Django admin. See the Admin guide for usage, the tenant switcher, and the middleware interaction.
django_rls_tenants.tenants.admin.RLSTenantModelAdmin
¶
Bases: ModelAdmin
ModelAdmin mixin that scopes every admin DB operation to the RLS context.
Register it like any ModelAdmin::
from django.contrib import admin
from django_rls_tenants import RLSTenantModelAdmin
from myapp.models import Order
@admin.register(Order)
class OrderAdmin(RLSTenantModelAdmin):
list_display = ("product", "amount")
request.user must satisfy the
:class:~django_rls_tenants.tenants.types.TenantUser protocol
(is_tenant_admin / rls_tenant_id) -- the same contract the middleware
and for_user() rely on.
Attributes:
| Name | Type | Description |
|---|---|---|
rls_allow_tenant_switch |
bool
|
Give cross-tenant admins the tenant switcher.
When |
rls_session_key |
str
|
Session key holding the switcher selection. Defaults to
|
rls_tenant_query_param |
str
|
Query parameter the switcher uses. Defaults to
|
rls_deny_without_tenant |
bool
|
When |
changelist_view
¶
changelist_view(
request: HttpRequest,
extra_context: dict[str, Any] | None = None,
) -> HttpResponse
Wrap the changelist in the request's RLS context.
Source code in django_rls_tenants/tenants/admin.py
add_view
¶
add_view(
request: HttpRequest,
form_url: str = "",
extra_context: dict[str, Any] | None = None,
) -> HttpResponse
Wrap the add view in the request's RLS context.
Source code in django_rls_tenants/tenants/admin.py
change_view
¶
change_view(
request: HttpRequest,
object_id: str,
form_url: str = "",
extra_context: dict[str, Any] | None = None,
) -> HttpResponse
Wrap the change view in the request's RLS context.
Source code in django_rls_tenants/tenants/admin.py
delete_view
¶
delete_view(
request: HttpRequest,
object_id: str,
extra_context: dict[str, Any] | None = None,
) -> HttpResponse
Wrap the delete view in the request's RLS context.
Source code in django_rls_tenants/tenants/admin.py
history_view
¶
history_view(
request: HttpRequest,
object_id: str,
extra_context: dict[str, Any] | None = None,
) -> HttpResponse
Wrap the history view in the request's RLS context.
Source code in django_rls_tenants/tenants/admin.py
get_exclude
¶
Hide the tenant FK when the effective tenant is implicit.
For a scoped user, or an admin who has selected a tenant, the tenant is
known and set by :meth:save_model, so the field is excluded. A global
admin with no selection keeps it visible to choose a tenant explicitly.
Source code in django_rls_tenants/tenants/admin.py
get_fieldsets
¶
Drop the tenant FK from explicit fieldsets when the tenant is implicit.
Mirrors :meth:get_exclude: when the effective tenant is known the FK is
excluded from the form, so a fieldsets layout that still names it would
raise KeyError at render time. A global admin with no selection keeps
the field (and its layout slot) so they can assign a tenant explicitly.
Source code in django_rls_tenants/tenants/admin.py
save_model
¶
Stamp the effective tenant onto the object before saving.
When the tenant FK is hidden (implicit tenant) the form never sets it, so
it is assigned here. When the effective tenant is None (global admin
viewing all) the field is visible and the admin's own choice stands.
Source code in django_rls_tenants/tenants/admin.py
get_list_filter
¶
Prepend the tenant switcher for switch-capable admins.
Source code in django_rls_tenants/tenants/admin.py
django_rls_tenants.tenants.admin.TenantSwitchListFilter
¶
TenantSwitchListFilter(
request: HttpRequest,
params: dict[str, list[str]],
model: type[Model],
model_admin: ModelAdmin[Any],
)
Bases: SimpleListFilter
Changelist filter that renders the cross-tenant admin's tenant switcher.
The dropdown lists every tenant from RLS_TENANTS["TENANT_MODEL"] plus an
"All" entry. Selecting an option reloads the changelist with
?<param>=<tenant-pk> (or ?<param>=__all__); the owning
:class:RLSTenantModelAdmin persists that choice to the session and applies
the actual scoping by activating the matching context. The filter's own
:meth:queryset is therefore a no-op -- it exists to render the control
and to register the query parameter so the changelist accepts it.
Only attached for users who may switch tenants (see
:meth:RLSTenantModelAdmin.get_list_filter).
Source code in django_rls_tenants/tenants/admin.py
lookups
¶
Return (pk, label) pairs for every tenant.
The tenant model is global (not RLS-protected), so this is unaffected by the active context and always lists all tenants.
Source code in django_rls_tenants/tenants/admin.py
value
¶
Return the selected tenant id.
The query parameter wins; otherwise fall back to the session-persisted selection so the dropdown still shows the active tenant on add/change pages that carry no query string.
Source code in django_rls_tenants/tenants/admin.py
queryset
¶
choices
¶
Yield the "All" entry (clearing the selection) followed by each tenant.
Source code in django_rls_tenants/tenants/admin.py
Types¶
django_rls_tenants.tenants.types.TenantUser
¶
Bases: Protocol
Protocol that user objects must satisfy for RLS context resolution.
Implement these two properties on your User model::
class User(AbstractUser, RLSProtectedModel):
@property
def is_tenant_admin(self) -> bool:
return self.role.name == "ADMIN"
@property
def rls_tenant_id(self) -> int | str | None:
return self.tenant_id if self.tenant_id else None
Bypass Helpers¶
django_rls_tenants.tenants.bypass.set_bypass_flag
¶
Set a bypass flag on the current database connection.
The flag name should match one of the extra_bypass_flags
configured on an RLSConstraint::
set_bypass_flag("rls.is_login_request")
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flag_name
|
str
|
GUC variable name (e.g., |
required |
is_local
|
bool
|
If |
False
|
using
|
str
|
Database alias. Default: |
'default'
|
Source code in django_rls_tenants/tenants/bypass.py
django_rls_tenants.tenants.bypass.clear_bypass_flag
¶
Clear a bypass flag on the current database connection.
Testing¶
django_rls_tenants.tenants.testing.rls_bypass
¶
Temporarily enable admin bypass for tests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
using
|
str
|
Database alias. Default: |
'default'
|
Usage::
with rls_bypass():
all_orders = Order.objects.all() # sees all tenants
Source code in django_rls_tenants/tenants/testing.py
django_rls_tenants.tenants.testing.rls_as_tenant
¶
Scope to a specific tenant for tests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
int | str
|
The tenant PK to scope to. |
required |
using
|
str
|
Database alias. Default: |
'default'
|
Usage::
with rls_as_tenant(tenant_id=42):
orders = Order.objects.all() # only tenant 42
Source code in django_rls_tenants/tenants/testing.py
django_rls_tenants.tenants.testing.assert_rls_enabled
¶
Assert that RLS is enabled and forced on the given table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
table_name
|
str
|
The database table name to check. |
required |
using
|
str
|
Database alias. Default: |
'default'
|
Raises:
| Type | Description |
|---|---|
AssertionError
|
If RLS is not enabled or not forced. |
Source code in django_rls_tenants/tenants/testing.py
django_rls_tenants.tenants.testing.assert_rls_policy_exists
¶
assert_rls_policy_exists(
table_name: str,
policy_name: str | None = None,
*,
using: str = "default",
) -> None
Assert that an RLS policy exists on the given table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
table_name
|
str
|
The database table name to check. |
required |
policy_name
|
str | None
|
Expected policy name. Defaults to
|
None
|
using
|
str
|
Database alias. Default: |
'default'
|
Source code in django_rls_tenants/tenants/testing.py
django_rls_tenants.tenants.testing.assert_rls_blocks_without_context
¶
Assert that querying with no GUC context returns zero rows.
Verifies the fail-closed behavior. Requires at least one row to exist in the table (caller must set up test data first).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_class
|
type[Model]
|
The RLS-protected model class to query. |
required |
using
|
str
|
Database alias. Default: |
'default'
|
Raises:
| Type | Description |
|---|---|
AssertionError
|
If any rows are returned, or if the table is empty (which would make the assertion pass vacuously). |
Source code in django_rls_tenants/tenants/testing.py
Contrib¶
Optional integrations with third-party libraries. These are not re-exported from the top-level package; import them from their module.
Celery¶
Native Celery integration. Requires the celery extra
(pip install django-rls-tenants[celery]); import from
django_rls_tenants.contrib.celery. See the Celery Tasks guide
for usage, chains/groups, and the install() escape hatch.
django_rls_tenants.contrib.celery.rls_task
¶
Define a Celery task that propagates the RLS context. Like shared_task.
A thin wrapper over :func:celery.shared_task that defaults base to
:class:RLSTask. Use it exactly as you would shared_task -- bare or with
options::
from django_rls_tenants.contrib.celery import rls_task
@rls_task
def reindex(): ...
@rls_task(bind=True, max_retries=3)
def sync(self): ...
Enqueue inside an RLS context and the worker runs the body scoped to the same tenant::
with tenant_context(tenant.pk):
reindex.delay() # runs on the worker under tenant_context(tenant.pk)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*args
|
Any
|
Forwarded to |
()
|
**options
|
Any
|
Forwarded to |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
The shared task, or a decorator when called with options. |
Source code in django_rls_tenants/contrib/celery.py
django_rls_tenants.contrib.celery.RLSTask
¶
Bases: Task
Celery task base that carries the RLS context from caller to worker.
On enqueue (apply_async on a real broker, or apply in eager mode and
for canvas steps) the active tenant/admin context is captured into the task
headers. On the worker, :meth:__call__ reads those headers and runs the
body inside the matching tenant_context() / admin_context(), which
restores cleanly on both success and exception.
Prefer the :func:rls_task decorator; subclass or pass
shared_task(base=RLSTask) only when you need a custom base.
Attributes:
| Name | Type | Description |
|---|---|---|
rls_require_context |
bool
|
When |
apply_async
¶
Capture the active context into headers, then enqueue normally.
Source code in django_rls_tenants/contrib/celery.py
apply
¶
Capture context for eager execution and for canvas steps.
Eager mode and canvas (chain/group) dispatch call apply directly
rather than going through apply_async, so the capture is wired in
here as well to cover those paths.
Source code in django_rls_tenants/contrib/celery.py
__call__
¶
Restore the propagated RLS context around the task body.
Source code in django_rls_tenants/contrib/celery.py
django_rls_tenants.contrib.celery.install
¶
Globally propagate RLS context for all Celery tasks, via signals.
Connects before_task_publish (capture into headers) and
task_prerun / task_postrun (restore around the body) so context flows
even for tasks that are not based on :class:RLSTask. Use it as an escape
hatch for third-party or legacy tasks you cannot re-base.
Prefer :func:rls_task / :class:RLSTask where you can: they restore the
context for the whole body including its own apply_async calls, are
scoped per task, and need no global wiring. install() and the base class
compose safely -- RLSTask instances are skipped by the signal handlers.
Call it once during startup (for example in your Celery app module). It is
idempotent: a repeated call does not double-connect. Reverse it with
:func:uninstall.
Source code in django_rls_tenants/contrib/celery.py
django_rls_tenants.contrib.celery.uninstall
¶
Disconnect the signal handlers connected by :func:install.
Idempotent: safe to call when :func:install was never called. Does not
affect tasks based on :class:RLSTask, which never relied on the signals.
Any contexts still open from :func:_task_prerun are exited here as a
best-effort cleanup so a stale context cannot leak into the next task.
Warning
Do not call this while tasks are still executing on other threads. A
ContextVar token can only be reset on the thread that created it, and
Django database connections are thread-local, so this cleanup only
unwinds contexts entered on the calling thread -- an in-flight task on a
worker thread keeps its context until it finishes. Call uninstall()
at shutdown, or from the worker thread between tasks.