Skip to main content

Policies

A policy renders the USING / WITH CHECK predicate of a CREATE POLICY statement. All policies share these keyword arguments:

ArgumentDefaultMeaning
operation"ALL"ALL, SELECT, INSERT, UPDATE, or DELETE
permissiveTruepermissive (OR-combined) vs restrictive (AND-combined)
roles"public"the policy's TO clause — PUBLIC or a comma-separated list

USING and WITH CHECK are emitted only for the operations PostgreSQL allows: USING for SELECT/UPDATE/DELETE/ALL, WITH CHECK for INSERT/UPDATE/ALL.

TenantPolicy

Isolate rows by a tenant column:

from flask_rls import TenantPolicy

TenantPolicy("tenant_isolation", "tenant_id")
TenantPolicy("tenant_isolation", "tenant_id", cast="uuid") # cast the GUC

Generates tenant_id = (SELECT NULLIF(current_setting('rls.tenant_id', true), '')::text). The cast is configurable and defaults to text.

UserPolicy

Isolate rows by an owning-user column, reading rls.user_id:

from flask_rls import UserPolicy

UserPolicy("owner_only", "user_id")

ExpressionPolicy

The Pythonic policy — compose a predicate from SQLAlchemy Core expressions, using RLS.tenant_id() / RLS.user_id() as the context references:

from flask_rls import ExpressionPolicy, RLS
from sqlalchemy import column, true

ExpressionPolicy(
"project_access",
expr=(column("owner_id") == RLS.user_id()) | (column("is_public") == true()),
)

The expression is compiled to SQL by SQLAlchemy's own PostgreSQL compiler, so identifiers are quoted and literals rendered by the dialect — safer than hand-writing SQL.

CustomPolicy

An escape hatch for a raw predicate. The expression is checked against obvious injection and DDL payloads, but prefer ExpressionPolicy where possible:

from flask_rls import CustomPolicy

CustomPolicy("recent_only", "created_at > now() - interval '30 days'")

Performance: the InitPlan pattern

The current_setting read is wrapped in a scalar subquery ((SELECT NULLIF(current_setting(...), '')::type)) so PostgreSQL evaluates it once per statement (an InitPlan) rather than once per candidate row — the documented RLS performance pattern, predicate-equivalent to the bare call.

The owner-bypass gotcha

A table's owner and any superuser bypass RLS entirely unless the table has FORCE ROW LEVEL SECURITY. Flask RLS emits FORCE for every registered table, and you should connect your application as a non-owner role. Otherwise your policies exist but do nothing.

Validation

Every identifier that reaches generated DDL — table, field, role, GUC, cast — is validated against a strict pattern before interpolation, and a malformed value raises PolicyError.