Skip to content

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


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 where expr is false are invisible.
  • WITH CHECK (expr) — evaluated on INSERT/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 PERMISSIVEEffect: policy_a OR policy_b
Two policies, one PERMISSIVE, one RESTRICTIVEpermissive_set AND restrictive_set
Two policies, both RESTRICTIVErestrictive_a AND restrictive_b
FOR ALLApplies to every command
FOR SELECT onlyOther commands unaffected (i.e. allowed)
Multiple FOR SELECTOR-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

-- Schema
CREATE 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