prax-orm 0.11.0

A next-generation, type-safe ORM for Rust inspired by Prisma
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
---
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>