Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.2.0 - 2026-03-21¶
Added¶
- M2M join table RLS support (#11): subquery-based RLS policies for auto-generated M2M through tables, ensuring tenant isolation extends to many-to-many relationships.
RLSM2MConstraint: migration-awareBaseConstraintthat generatesEXISTS-based subquery policies on M2M join tables. Supports both-sides-protected, one-side-protected, and self-referential M2M.AddM2MRLSPolicy: reversible Django migration operation for applying M2M RLS policies. All inputs validated against SQL injection.register_m2m_rls(): auto-detection inAppConfig.ready()that discovers M2M fields onRLSProtectedModelsubclasses and registersRLSM2MConstrainton their auto-generated through tables.setup_m2m_rlsmanagement command for retroactive M2M RLS application on existing deployments (with--dry-runand--databaseflags).check_rlsnow also verifies RLS on M2M through tables.STRICT_MODEconfiguration option. When enabled,TenantQuerySetevaluation methods (count(),exists(),aggregate(),update(),delete(),iterator(),bulk_create(),bulk_update(),get(),first(),last(), and iteration via_fetch_all()) raiseNoTenantContextErrorif no RLS context is active. Off by default. (#13)_rls_context_activeContextVar instate.pywith public accessorsget_rls_context_active(),set_rls_context_active(), andreset_rls_context_active(). Tracks whether any RLS context (tenant or admin) is active, enabling strict mode to distinguish "no context" from "admin context". (#13)- Custom exception hierarchy in
django_rls_tenants.exceptions:RLSTenantError(base),NoTenantContextError,RLSConfigurationError. All importable from the top-leveldjango_rls_tenantspackage. (#12) DATABASESconfiguration option for multi-database GUC support. The middleware now sets GUCs on all configured database aliases, not justdefault. Default:["default"](backward compatible). (#9)connection_createdsignal handler that sets GUCs on lazily created database connections mid-request (e.g., replica connections opened by a database router).check_rls --databaseflag for verifying RLS on non-default databases.- System checks
W006(invalid database alias inDATABASES) andW007(USE_LOCAL_SET=TruewithoutATOMIC_REQUESTSon configured databases).
Changed¶
tenant_context(),admin_context(),RLSTenantMiddleware, andfor_user()now set_rls_context_active=Trueon entry and restore the previous value on exit. This enables strict mode's "no context" detection. (#13)tenant_context()and_resolve_user_guc_vars()now raiseNoTenantContextErrorinstead ofValueErrorwhen a non-admin user hasrls_tenant_id=Noneor whentenant_idisNone.@with_rls_contextdecorator now raisesNoTenantContextErrorinstead ofValueErrorwhen a non-admin user hasrls_tenant_id=None.RLSTenantsConfig._get()now raisesRLSConfigurationErrorinstead ofValueErrorwhen a required config key (e.g.,TENANT_MODEL) is missing.
1.1.0 - 2026-03-17¶
Added¶
get_current_tenant_id()/set_current_tenant_id()/reset_current_tenant_id()functions for custom middleware and management commands that need direct access to the auto-scope state. Use the token returned byset_current_tenant_id()withreset_current_tenant_id(token)to safely restore the previous value.W005system check that warns when the default database connection uses a PostgreSQL superuser. Superusers bypass all RLS policies, completely disabling tenant isolation.
Changed¶
- Automatic query scoping:
RLSManager.get_queryset()now addsWHERE tenant_id = Xautomatically when a tenant context is active (viatenant_context(),admin_context(), orRLSTenantMiddleware). This enables PostgreSQL to use composite indexes, eliminating the sequential scan penalty of RLScurrent_setting()calls. No code changes required -- activates automatically.for_user()continues to work as before. - RLS policy rewrite:
RLSConstraintnow generatesCASE WHENpolicies instead ofOR-based policies, improving readability and clarifying the evaluation structure. Existing policies are updated on the next migration. (Note: the primary performance improvement comes from auto-scoping above, which enables index usage. TheCASE WHENrewrite is a clarity improvement, not a performance optimization.) TenantQuerySet.select_related()now auto-propagates tenant filters to joined RLS-protected tables when a tenant context is active.- Middleware GUC-set tracking now uses
ContextVarinstead ofthreading.local, ensuring proper isolation in ASGI (async) deployments where multiple coroutines share a single thread. - Middleware adds
process_exception()handler that cleans up bothContextVarstate and GUCs when an unhandled view exception preventsprocess_responsefrom running. request_finishedsafety-net signal handler now also resets theContextVarauto-scope state, not just the GUC variables._resolve_user_guc_vars()now raisesValueErrorfor non-admin users withrls_tenant_id=Noneinstead of stringifyingNone. This catches user-model misconfigurations at middleware/context-manager time rather than producing a silent mismatch at the database level.@with_rls_contextnow validatesrls_tenant_idbefore entering the tenant context (fail-fast), providing a clear error message that includes the function name.
Fixed¶
_add_tenant_fksignal handler now reads the configuredTENANT_FK_FIELDvalue instead of hardcoding"tenant"when checking for an existing field. Previously, a customTENANT_FK_FIELD(e.g.,"organization") would cause the handler to miss existing fields and attempt to add a duplicate FK.W004system check now correctly detectsCONN_MAX_AGE=None(Django's "keep connections forever" sentinel). Previously, only positive integer values were flagged;Nonesilently passed the check despite being the most dangerous value for GUC leak risk.
1.0.0 - 2026-03-15¶
Initial stable release of django-rls-tenants.
Added¶
Architecture¶
- Two-layer architecture with strict import boundary enforced by tests:
rls/layer: generic PostgreSQL RLS primitives with zero Django model knowledge.tenants/layer: opinionated Django multitenancy built on therls/layer.
RLS Primitives (rls/)¶
set_guc,get_guc,clear_guchelpers for managing PostgreSQL session variables (GUCs), with regex-based SQL injection prevention.RLSConstraintfor generatingCREATE POLICY/DROP POLICYDDL in Django migrations, with support forint,bigint, anduuidprimary key types.rls_contextgeneric context manager for setting/clearing arbitrary GUC variables with save/restore nesting support.bypass_flagcontext manager for toggling boolean bypass flags within a transaction-scoped context.
Tenant Models & Managers (tenants/)¶
RLSProtectedModelabstract base class with dynamic tenant foreign key added via theclass_preparedsignal. Supports auto-generated and explicit FK configurations.TenantQuerySetwith lazy GUC evaluation at query execution time (not queryset creation time), solving the lazy evaluation problem for chained queries.RLSManagerwithfor_user()for scoped queries andprepare_tenant_in_model_data()for efficient bulk creation without N+1SELECTqueries.- Defense-in-depth:
for_user()applies both Django ORM.filter()and database-level RLS, so even if one layer is bypassed the other provides isolation.
Middleware & Context¶
RLSTenantMiddlewarefor automatic per-request RLS context based on the authenticated user. API-agnostic (works with REST, GraphQL, Django views).tenant_contextandadmin_contextcontext managers with nesting support and automatic GUC cleanup viatry/finally.@with_rls_contextdecorator for extracting user context from function arguments, with configurableuser_paramand fail-closed behavior.request_finishedsignal safety net for GUC cleanup in case middleware'sprocess_responsedoes not run.
Configuration & Validation¶
- Single
RLS_TENANTSsettings dict with 6 configuration keys:TENANT_MODEL,TENANT_FK_FIELD,USER_PARAM_NAME,GUC_PREFIX,TENANT_PK_TYPE,USE_LOCAL_SET. RLSTenantsConfigsingleton with lazy property access and unknown-key detection (warns on typos).- Django system checks:
W001(GUC prefix mismatch for tenant),W002(GUC prefix mismatch for admin),W003(USE_LOCAL_SETwithoutATOMIC_REQUESTS),W004(CONN_MAX_AGE > 0with session-scoped GUCs).
User Integration¶
TenantUserruntime-checkableProtocolfor structural subtyping of user objects. Requiresis_tenant_adminandrls_tenant_idproperties.
Bypass Mode¶
bypass_flagcontext manager for temporary bypass of specific RLS policies.set_bypass_flag/clear_bypass_flagimperative helpers.extra_bypass_flagssupport onRLSConstraintfor custom bypass clauses (e.g., login flows).
Connection Pooling¶
USE_LOCAL_SETconfiguration for transaction-scoped GUCs viaSET LOCAL, compatible with PgBouncer and other connection poolers.
Management Commands¶
check_rlsmanagement command to verify RLS policies are correctly applied to all protected models, with CI-friendly exit codes.
Testing Utilities¶
rls_bypasscontext manager for disabling RLS in test setup/teardown.rls_as_tenantcontext manager for running tests as a specific tenant.assert_rls_enabledassertion for verifying RLS is active on a table.assert_rls_policy_existsassertion for verifying a named policy exists.assert_rls_blocks_without_contextassertion for verifying fail-closed behavior.
Package & Tooling¶
- PEP 561
py.typedmarker for typed package support. __version__attribute viaimportlib.metadata(single source of truth).- Full MkDocs Material documentation site with 19 pages: getting started, guides, advanced topics, API reference, and development docs.
- CI pipeline: Python {3.11, 3.12, 3.13, 3.14} x Django {4.2, 5.0, 5.1, 5.2, 6.0} test matrix with PostgreSQL 16.
- OIDC trusted publishing to PyPI via GitHub Actions.
- Security policy (
SECURITY.md) with vulnerability disclosure process.
Fixed¶
clear_gucnow accepts anis_localparameter, ensuring consistent GUC lifetimes whenUSE_LOCAL_SET=True. Previously,admin_context, middleware, and manager_fetch_allcould clear GUCs with session scope while setting them with transaction scope, causing mismatched lifetimes.