Advanced Row-Level Security Policies
Advanced Row-Level Security Policies
Available since: v3.2.0
Build: default — no feature flag required
Modules: heliosdb_lite::tenant (RLSPolicy, RLSCommand, RLSExpressionEvaluator)
UVP
PostgreSQL-shaped RLS without the overhead — type-safe RLSPolicy records with explicit USING and WITH CHECK expressions, four command targets (SELECT, INSERT, UPDATE, DELETE, plus All), and an evaluator that walks LogicalExpr against the live tuple and the request’s TenantContext. Multiple policies on one table compose with OR; combine with FORCE for table-owner enforcement; pull current_user, current_setting, and tenant claims directly into the predicate. RLS is the bedrock of multi-tenant isolation in Lite — and it’s a single CREATE POLICY away.
Prerequisites
- HeliosDB Lite v3.2+
- Familiarity with RLS_POLICY_MANAGEMENT_TUTORIAL basics
- 20 minutes
1. Anatomy of an RLSPolicy
pub struct RLSPolicy { pub name: String, // "tenant_isolation" pub table_name: String, // "documents" pub condition: String, // human-readable summary pub cmd: RLSCommand, // Select | Insert | Update | Delete | All pub using_expr: String, // gates which rows are visible pub with_check_expr: Option<String>, // gates writes (INSERT/UPDATE)}USING and WITH CHECK look identical but apply at different times:
USING (expr)— evaluated at scan time. Rows whereexpris false are invisible.WITH CHECK (expr)— evaluated onINSERT/UPDATE. Rows that would violate the check are rejected.
If you write USING and omit WITH CHECK, the same predicate applies to writes (PostgreSQL semantics).
2. Enabling RLS on a Table
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;Owners are exempt unless you also force it:
ALTER TABLE documents FORCE ROW LEVEL SECURITY;The FORCE form is the multi-tenant default — even the table owner needs to satisfy the policy.
3. Standard Patterns
Tenant isolation
CREATE POLICY tenant_isolation ON documents AS PERMISSIVE FOR ALL USING (tenant_id = current_setting('app.tenant_id')::uuid);current_setting('app.tenant_id') is set per-request by the auth layer (the embedded server, the REST handler, or your application code via set_current_context).
Read-only public data + writeable owned data
Two policies on one table compose with OR:
-- Anyone can SELECT a public document.CREATE POLICY public_select ON documents AS PERMISSIVE FOR SELECT USING (visibility = 'public');
-- Owners can do anything with their own documents.CREATE POLICY owner_full ON documents AS PERMISSIVE FOR ALL USING (owner_id = current_setting('app.user_id')::uuid) WITH CHECK (owner_id = current_setting('app.user_id')::uuid);A SELECT is allowed if either policy permits it; an INSERT succeeds only if the WITH CHECK of the matching policy passes.
Restrictive policies (AND)
AS RESTRICTIVE makes the policy AND-combine instead of OR-combine. Useful for “must satisfy both privacy and tenancy”:
CREATE POLICY pii_redaction ON users AS RESTRICTIVE FOR SELECT USING (NOT pii_only OR has_role('compliance'));A user with compliance role bypasses pii_only; everyone else has the row hidden.
Role-aware policies
CREATE POLICY admins_see_all ON audit_log FOR SELECT USING (current_setting('app.user_role') = 'admin');
CREATE POLICY users_own_only ON audit_log FOR SELECT USING (user_id = current_setting('app.user_id')::uuid);Either policy can satisfy the SELECT; admins get the unrestricted view.
4. The Rust API
tenant::TenantManager::create_rls_policy registers a policy in the in-memory registry:
use heliosdb_lite::tenant::{TenantManager, RLSCommand};
let manager = TenantManager::new();
manager.create_rls_policy( "documents".to_string(), // table "tenant_isolation".to_string(), // policy name "tenant_id = current_tenant()".to_string(), // condition (summary) RLSCommand::All, // applies to every cmd "tenant_id = current_tenant()".to_string(), // USING Some("tenant_id = current_tenant()".to_string()), // WITH CHECK);To remove a policy:
manager.remove_rls_policy("documents", "tenant_isolation");To list:
for p in manager.get_rls_policies("documents") { println!("{}: USING ({})", p.name, p.using_expr);}5. The Expression Evaluator
RLSExpressionEvaluator parses the using_expr / with_check_expr strings via sqlparser (PostgreSQL dialect) and emits a LogicalExpr that walks against the schema and the request’s TenantContext:
use heliosdb_lite::tenant::{RLSExpressionEvaluator, TenantContext};use std::sync::Arc;
let evaluator = RLSExpressionEvaluator::new( Arc::clone(&schema), Some(tenant_context.clone()),);
let expr = evaluator.parse("tenant_id = current_tenant()")?;// `expr` is a heliosdb_lite::sql::LogicalExpr you can hand to the planner.Compound identifiers (schema.table.column) are preserved with table qualifiers — joins work without ambiguity. The evaluator supports the full sqlparser::ast::Expr surface (binary ops, unary ops, function calls, IN, LIKE, etc.) — anything you can put in a SQL WHERE clause is fair game in a policy.
6. Composition Reference
Two policies, both PERMISSIVE | Effect: policy_a OR policy_b |
|---|---|
Two policies, one PERMISSIVE, one RESTRICTIVE | permissive_set AND restrictive_set |
Two policies, both RESTRICTIVE | restrictive_a AND restrictive_b |
FOR ALL | Applies to every command |
FOR SELECT only | Other commands unaffected (i.e. allowed) |
Multiple FOR SELECT | OR-combined for visible rows |
Match the table-owner contract: if FORCE is on, the owner is also subject to the same set.
7. Putting It Together — Multi-Tenant SaaS
-- SchemaCREATE TABLE documents ( id UUID PRIMARY KEY, tenant_id UUID NOT NULL, owner_id UUID NOT NULL, title TEXT, body TEXT, visibility TEXT NOT NULL CHECK (visibility IN ('private', 'team', 'public')));
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;ALTER TABLE documents FORCE ROW LEVEL SECURITY;
-- 1. Tenancy is mandatory.CREATE POLICY tenant_only ON documents AS RESTRICTIVE FOR ALL USING (tenant_id = current_setting('app.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
-- 2. Public docs visible to anyone in the tenant.CREATE POLICY public_read ON documents AS PERMISSIVE FOR SELECT USING (visibility = 'public');
-- 3. Owner can do anything with their own.CREATE POLICY owner_full ON documents AS PERMISSIVE FOR ALL USING (owner_id = current_setting('app.user_id')::uuid) WITH CHECK (owner_id = current_setting('app.user_id')::uuid);
-- 4. Team members can read team-visible docs.CREATE POLICY team_read ON documents AS PERMISSIVE FOR SELECT USING ( visibility = 'team' AND owner_id IN ( SELECT user_id FROM team_membership WHERE team_id = current_setting('app.team_id')::uuid ) );The combined effect: every operation must satisfy the restrictive tenant_only AND at least one permissive policy.
Where Next
- MULTI_TENANT_QUOTAS_TUTORIAL — pair RLS with quota enforcement for full isolation.
- CDC_TUTORIAL — make sure your change feed honours the same boundaries.
- DATABASE_BRANCHING_TUTORIAL — per-tenant branches inherit RLS.
- RLS_POLICY_MANAGEMENT_TUTORIAL — the basics, if you’re new to RLS.
- RLS_QUICKSTART — 5-minute setup.