---
import DocsLayout from '../../layouts/DocsLayout.astro';
import CodeBlock from '../../components/CodeBlock.astro';
const rlsPolicy = `use prax_query::security::{PolicyCommand, RlsPolicy, RlsPolicyBuilder};
// Row-Level Security policy (PostgreSQL/MSSQL).
// The builder takes the policy name AND table; FOR ALL is the default command.
let tenant_policy = RlsPolicyBuilder::new("tenant_isolation", "orders")
.using("tenant_id = current_setting('app.tenant_id')::int")
.with_check("tenant_id = current_setting('app.tenant_id')::int")
.build();
// Generated SQL (PostgreSQL) via tenant_policy.to_postgres_sql():
// CREATE POLICY tenant_isolation ON orders AS PERMISSIVE FOR ALL
// USING (tenant_id = current_setting('app.tenant_id')::int)
// WITH CHECK (tenant_id = current_setting('app.tenant_id')::int)
//
// Note: enabling RLS is a separate statement — run it yourself:
// ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
// Command-scoped policy
let read_policy = RlsPolicyBuilder::new("read_own", "orders")
.for_select() // also .for_insert() / .for_update() / .for_delete()
// or .for_command(PolicyCommand::All)
.using("owner_id = current_setting('app.user_id')::int")
.build();
// Permissive policy (the default; OR'd with other permissive policies)
let admin_policy = RlsPolicyBuilder::new("admin_access", "orders")
.for_select()
.permissive()
.using("current_setting('app.role') = 'admin'")
.build();
// Restrictive policy (AND'd with other policies)
let active_only = RlsPolicyBuilder::new("active_only", "orders")
.restrictive()
.using("status != 'deleted'")
.build();`;
const multiTenant = `use prax_query::security::{TenantPolicy, TenantSource};
use prax_query::DatabaseType;
// Multi-tenant RLS setup — one policy per table.
// TenantPolicy::new(table, tenant_column, source) returns the policy
// directly (no builder chain).
let policies = ["users", "orders", "products", "invoices"].map(|table| {
TenantPolicy::new(table, "tenant_id", TenantSource::SessionVar("app.tenant_id".into()))
});
// Generate CREATE POLICY SQL for each table
for policy in &policies {
let rls = policy.to_postgres_rls();
conn.execute_batch(&rls.to_postgres_sql()).await?;
// (plus ALTER TABLE .. ENABLE ROW LEVEL SECURITY — see above)
}
// Per-request: scope the session, then the database filters every query
let orders_policy = &policies[1];
let set_sql = orders_policy.set_tenant_sql("42", DatabaseType::PostgreSQL);
// SET LOCAL app.tenant_id = '42'
conn.execute(&set_sql, &[]).await?;
let orders = client.order().find_many().exec().await?; // only tenant 42's orders
// TenantSource variants:
// SessionVar("app.tenant_id") — Postgres current_setting('app.tenant_id')
// SessionContext("tenant_id") — MSSQL SESSION_CONTEXT
// Function("current_tenant") — custom function call`;
const roleManagement = `use prax_query::security::{Role, RoleBuilder};
// Create application roles.
// login() / createdb() / createrole() take no argument;
// inherit(..) takes the list of roles to inherit from.
let reader = RoleBuilder::new("app_reader").build();
let writer = RoleBuilder::new("app_writer")
.inherit(["app_reader"])
.build();
let admin = RoleBuilder::new("app_admin")
.inherit(["app_writer"])
.createdb()
.createrole()
.build();
// Create a login role
let api_user = RoleBuilder::new("api_service")
.login()
.password("secure_password")
.inherit(["app_writer"])
.connection_limit(10)
.valid_until("2026-12-31")
.build();
// PostgreSQL via role.to_postgres_sql():
// CREATE ROLE app_reader WITH NOLOGIN;
// CREATE ROLE app_writer WITH NOLOGIN IN ROLE app_reader;
// CREATE ROLE app_admin WITH NOLOGIN CREATEDB CREATEROLE IN ROLE app_writer;
// CREATE ROLE api_service WITH LOGIN PASSWORD 'secure_password'
// CONNECTION LIMIT 10 VALID UNTIL '2026-12-31' IN ROLE app_writer;`;
const grants = `use prax_query::security::{Grant, GrantBuilder, GrantObject, Privilege};
// GrantBuilder::new takes the grantee up front; privileges are added
// one at a time; .build() returns QueryResult<Grant>.
// Table-level grants
let table_grant = GrantBuilder::new("app_writer")
.select()
.insert()
.update()
.on_table("orders")
.build()?;
// Column-level grants (fine-grained access) — table first, then columns
let column_grant = GrantBuilder::new("support_staff")
.select()
.on_columns("users", ["email", "name"])
.build()?;
// Schema grants via the generic .privilege(..)
let schema_grant = GrantBuilder::new("api_service")
.privilege(Privilege::Usage)
.on_schema("api")
.build()?;
// Other objects via GrantObject (sequences, databases, functions, ...)
let seq_grant = GrantBuilder::new("app_writer")
.privilege(Privilege::Usage)
.on(GrantObject::Sequence("orders_id_seq".into()))
.build()?;
// WITH GRANT OPTION (allow grantee to grant to others)
let delegated = GrantBuilder::new("data_admin")
.select()
.on_table("public_data")
.with_grant_option()
.build()?;
// Privilege variants: Select, Insert, Update, Delete, Truncate,
// References, Trigger, All, Execute, Usage, Create, Connect`;
const dataMasking = `use prax_query::security::{DataMask, MaskFunction};
// Dynamic data masking (MSSQL / PostgreSQL view-based).
// DataMask::new takes (table, column, mask) and returns Self directly.
let email_mask = DataMask::new("users", "email", MaskFunction::Email);
// Partial masking — show a prefix/suffix around fixed padding
let phone_mask = DataMask::new(
"customers",
"phone",
MaskFunction::Partial {
prefix: 0,
padding: "XXXXXXX".to_string(),
suffix: 4,
},
); // XXXXXXX1234
// Full (default) masking
let ssn_mask = DataMask::new("employees", "ssn", MaskFunction::Default);
// Custom masking expression (used verbatim as the MSSQL mask function)
let custom = DataMask::new(
"orders",
"total",
MaskFunction::Custom("ROUND(total, -2)".into()),
);
// Generate DDL:
let view_sql = email_mask.to_postgres_view("users_masked");
let alter_sql = phone_mask.to_mssql_alter();`;
const connectionProfile = `use prax_query::security::ConnectionProfile;
// Named connection profiles with security settings.
// ConnectionProfile::new takes (name, role).
let readonly = ConnectionProfile::new("readonly", "app_reader")
.search_path(["public", "api"])
.statement_timeout(30_000) // milliseconds (u32)
.lock_timeout(5_000) // milliseconds (u32)
.read_only()
.build();
let api_profile = ConnectionProfile::new("api", "api_service")
.session_var("app.tenant_id", &tenant_id)
.session_var("app.user_id", &user_id)
.search_path(["tenant_schema", "public"])
.statement_timeout(60_000)
.build();
// Generate the session-setup SQL and apply it to a connection
for stmt in api_profile.to_postgres_setup() {
conn.execute_batch(&stmt).await?;
}
// SET ROLE api_service
// SET search_path TO tenant_schema, public
// SET statement_timeout = 60000
// SET app.tenant_id = '...'
// SET app.user_id = '...'`;
const mongoSecurity = `use prax_query::security::mongodb::{
EncryptionAlgorithm, FieldEncryption, KmsProviders, MongoRole,
};
// MongoDB role-based access control — MongoRole::new(role, db)
let analyst = MongoRole::new("analyst", "analytics")
.privilege_collection("reports", ["find", "aggregate"])
.privilege_collection("dashboards", ["find"])
.privilege_database(["listCollections"])
.inherit("read", "reporting")
.build();
// Serialize to a createRole command document
let cmd = analyst.to_create_command();
// Client-Side Field Level Encryption (CSFLE)
// FieldEncryption::new(key_vault_namespace, kms)
let encryption = FieldEncryption::new(
"encryption.__keyVault",
KmsProviders::Aws {
access_key_id: std::env::var("AWS_ACCESS_KEY_ID").unwrap(),
secret_access_key: std::env::var("AWS_SECRET_ACCESS_KEY").unwrap(),
region: "us-east-1".into(),
},
// Other KmsProviders variants:
// Local { key } — development only
// Azure { tenant_id, client_id, client_secret }
// Gcp { email, private_key }
)
.encrypt_field("mydb.users", "ssn", EncryptionAlgorithm::Deterministic, data_key_id)
.encrypt_field("mydb.users", "email", EncryptionAlgorithm::Random, data_key_id);
// Serialize to driver encryption options
let opts = encryption.to_options();`;
const tlsSecurity = `// prax-postgres — TLS is on via the default "tls" cargo feature.
// sslmode=require, verify-ca and verify-full establish rustls
// connections verified against the Mozilla root store (webpki-roots):
// certificate chain AND hostname are always checked (stricter than
// libpq's sslmode=require, which skips hostname verification).
let pool = PgPool::builder()
.url("postgres://user:pass@db.example.com/mydb?sslmode=verify-full")
.build()
.await?;
// Without the "tls" feature, TLS-requiring modes FAIL AT POOL BUILD —
// there is never a silent downgrade to plaintext.
// prax-mysql — SslMode (disabled / preferred / required / verify_ca /
// verify_identity) is wired to real mysql_async SslOpts:
// mysql://user:pass@host/db?ssl-mode=VERIFY_IDENTITY
// prax-scylladb — TLS via the "ssl" cargo feature (openssl SslContext).
// prax-cassandra — TLS with verify_hostname = true by default; setting
// it to false builds an encrypt-only context with NO certificate or
// hostname verification (development only).`;
---
<DocsLayout title="Security & Access Control - Prax ORM">
<article class="max-w-4xl mx-auto px-6 py-12">
<header class="mb-12">
<h1 class="text-4xl font-bold mb-4">Security & Access Control</h1>
<p class="text-xl text-muted">
Implement row-level security, role-based access, data masking, and field-level encryption.
</p>
</header>
<div class="space-y-12">
<!-- Introduction -->
<section>
<h2 class="text-2xl font-semibold mb-4">Overview</h2>
<p class="text-muted mb-4">
Prax provides comprehensive security features including Row-Level Security (RLS),
role management, fine-grained grants, and data masking.
</p>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-border">
<th class="text-left py-3 px-4 font-semibold">Feature</th>
<th class="text-left py-3 px-4 font-semibold">PostgreSQL</th>
<th class="text-left py-3 px-4 font-semibold">MySQL</th>
<th class="text-left py-3 px-4 font-semibold">SQLite</th>
<th class="text-left py-3 px-4 font-semibold">MSSQL</th>
<th class="text-left py-3 px-4 font-semibold">MongoDB</th>
</tr>
</thead>
<tbody class="text-muted">
<tr class="border-b border-border">
<td class="py-3 px-4">Row-Level Security</td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-muted">❌</span></td>
<td class="py-3 px-4"><span class="text-muted">❌</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span> Field-level</td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4">Column-Level Grants</td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-muted">❌</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4">Roles & Users</td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-muted">❌</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4">Data Masking</td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span> Enterprise</td>
<td class="py-3 px-4"><span class="text-muted">❌</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
</tr>
<tr class="border-b border-border">
<td class="py-3 px-4">Field Encryption</td>
<td class="py-3 px-4"><span class="text-success-400">✅</span> pgcrypto</td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-muted">❌</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span></td>
<td class="py-3 px-4"><span class="text-success-400">✅</span> CSFLE</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- RLS -->
<section>
<h2 class="text-2xl font-semibold mb-4">Row-Level Security (RLS)</h2>
<p class="text-muted mb-4">
Automatically filter rows based on security policies. Users only see data they're authorized to access.
</p>
<CodeBlock code={rlsPolicy} lang="rust" filename="src/security.rs" />
</section>
<!-- Multi-tenant -->
<section>
<h2 class="text-2xl font-semibold mb-4">Multi-Tenant Isolation</h2>
<p class="text-muted mb-4">
Implement tenant isolation with RLS policies tied to session variables (or session context / a custom function).
</p>
<CodeBlock code={multiTenant} lang="rust" filename="src/security.rs" />
<div class="mt-4 p-4 rounded-xl bg-success-500/10 border border-success-500/30">
<p class="text-success-400 text-sm">
<strong>Best Practice:</strong> Use RLS for tenant isolation instead of WHERE clauses.
It's enforced at the database level, preventing accidental data leaks in application code.
</p>
</div>
<div class="mt-4 p-4 rounded-xl bg-red-500/10 border border-red-500/30">
<p class="text-red-400 text-sm">
<strong>v0.11 caveat — JWT claims:</strong> there is no <code>TenantSource::JwtClaim</code>
variant. If you derive the tenant from a JWT, use
<code>prax_query::tenant::JwtClaimExtractor</code> (renamed
<code>UnverifiedJwtClaimExtractor</code> in v0.11; alias kept until 0.12) — but note it
performs <strong>no signature verification</strong> and must run behind a verifying auth
middleware. Never feed an unverified claim into a tenant context.
</p>
</div>
</section>
<!-- Roles -->
<section>
<h2 class="text-2xl font-semibold mb-4">Role Management</h2>
<p class="text-muted mb-4">
Create hierarchical roles with inherited privileges.
</p>
<CodeBlock code={roleManagement} lang="rust" filename="src/security.rs" />
</section>
<!-- Grants -->
<section>
<h2 class="text-2xl font-semibold mb-4">Grants & Privileges</h2>
<p class="text-muted mb-4">
Grant fine-grained permissions at table, column, or schema level.
</p>
<CodeBlock code={grants} lang="rust" filename="src/security.rs" />
</section>
<!-- Data Masking -->
<section>
<h2 class="text-2xl font-semibold mb-4">Dynamic Data Masking</h2>
<p class="text-muted mb-4">
Mask sensitive data for non-privileged users without changing the stored data.
</p>
<CodeBlock code={dataMasking} lang="rust" filename="src/security.rs" />
</section>
<!-- Connection Profiles -->
<section>
<h2 class="text-2xl font-semibold mb-4">Connection Profiles</h2>
<p class="text-muted mb-4">
Configure named connection profiles with security settings.
</p>
<CodeBlock code={connectionProfile} lang="rust" filename="src/security.rs" />
</section>
<!-- MongoDB -->
<section>
<h2 class="text-2xl font-semibold mb-4">MongoDB Security</h2>
<p class="text-muted mb-4">
MongoDB role-based access control and Client-Side Field Level Encryption (CSFLE).
</p>
<CodeBlock code={mongoSecurity} lang="rust" filename="src/mongodb.rs" />
</section>
<!-- Transport Security -->
<section>
<h2 class="text-2xl font-semibold mb-4">Transport Security (TLS)</h2>
<p class="text-muted mb-4">
Encrypted connections per backend, with fail-closed behavior when TLS support isn't compiled in.
</p>
<CodeBlock code={tlsSecurity} lang="rust" filename="TLS Configuration" />
</section>
<!-- Injection Hardening -->
<section>
<h2 class="text-2xl font-semibold mb-4">Injection Hardening Notes (v0.11)</h2>
<div class="space-y-3">
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-info-400">LIKE Wildcard Escaping</h4>
<p class="text-muted text-sm">
<code>contains</code> / <code>starts_with</code> / <code>ends_with</code> filters escape
<code>%</code> and <code>_</code> in user input and emit a dialect-aware
<code>ESCAPE</code> clause (<code>\</code> on Postgres/SQLite/MSSQL, <code>\\</code> on
MySQL). <strong>Behavior change:</strong> input that used to act as an intentional
wildcard now matches literally.
</p>
</div>
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-info-400">MongoDB Filter Parser</h4>
<p class="text-muted text-sm">
Raw-query field names starting with <code>$</code> (operator injection / server-side JS
such as <code>$where</code>) or containing <code>.</code> dot-notation are rejected.
</p>
</div>
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-info-400">Identifier & Startup-Option Validation</h4>
<p class="text-muted text-sm">
PostgreSQL URL options that become <code>-c key=value</code> startup assignments (e.g.
<code>search_path</code>) are validated — keys must match an identifier whitelist and
values containing whitespace or quotes are rejected. Savepoint names (which cannot be
parameterized) are validated against <code>^[A-Za-z_][A-Za-z0-9_]*$</code> and
length-capped.
</p>
</div>
</div>
</section>
<!-- Best Practices -->
<section>
<h2 class="text-2xl font-semibold mb-4">Best Practices</h2>
<div class="grid gap-4">
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-success-400">Principle of Least Privilege</h4>
<p class="text-muted text-sm">
Grant only the minimum permissions needed. Use role hierarchies to manage
permissions centrally and revoke easily.
</p>
</div>
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-success-400">Use RLS for Multi-Tenancy</h4>
<p class="text-muted text-sm">
RLS policies are enforced at the database level, making it impossible for
application bugs to leak data between tenants.
</p>
</div>
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-warning-400">Audit Security Changes</h4>
<p class="text-muted text-sm">
Log all role and permission changes. Use migrations for security changes
so they're version controlled and reviewable.
</p>
</div>
<div class="p-4 rounded-xl bg-surface border border-border">
<h4 class="font-semibold mb-2 text-info-400">Encrypt Sensitive Data</h4>
<p class="text-muted text-sm">
Use CSFLE (MongoDB) or pgcrypto (PostgreSQL) for sensitive fields.
Encryption protects data even if the database is compromised.
</p>
</div>
</div>
</section>
</div>
</article>
</DocsLayout>