Appearance
Rust — rusqlite
rusqlite loads extensions when built with the load_extension feature.
Cargo.toml
toml
[dependencies]
rusqlite = { version = "0.32", features = ["load_extension"] }
# add "bundled" to have rusqlite compile its own SQLiteLoad and set up
rust
use rusqlite::{Connection, LoadExtensionGuard};
fn main() -> rusqlite::Result<()> {
let conn = Connection::open("app.db")?;
// LoadExtensionGuard enables the loader and re-disables it on drop.
unsafe {
let _guard = LoadExtensionGuard::new(&conn)?;
conn.load_extension("dist/rls.so", None)?; // None => entry sqlite3_rls_init
}
conn.execute_batch(
"CREATE TABLE documents(id INTEGER PRIMARY KEY, owner_id INT NOT NULL, title TEXT);
INSERT INTO documents(owner_id, title) VALUES (1,'Alice'),(2,'Bob');",
)?;
conn.query_row(
"SELECT rls_protect(?1)",
[r#"{"table":"documents","select":"owner_id = rls_ctx('user_id')"}"#],
|r| r.get::<_, String>(0),
)?;
conn.query_row("SELECT rls_seal('CHANGE_ME')", [], |r| r.get::<_, i64>(0))?;
conn.query_row("SELECT rls_guard()", [], |r| r.get::<_, i64>(0))?;
Ok(())
}Serve queries
rust
conn.query_row("SELECT rls_set_context(?1)", [r#"{"user_id":1}"#], |r| r.get::<_, i64>(0))?;
let mut stmt = conn.prepare("SELECT id, title FROM documents")?;
let rows: Vec<(i64, String)> = stmt
.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?
.collect::<rusqlite::Result<_>>()?; // [(1, "Alice")]Important: don't set a second authorizer
One authorizer per connection
SQLite allows only one authorizer per connection, and this extension installs its own to make rls_guard() work. Calling conn.authorizer(...) on the same connection displaces RLS enforcement. If you need custom authorization, express it in the policy expressions, or run it on a separate connection.
Connection pools (r2d2 / deadpool)
Do the load + rls_protect + rls_config + rls_seal + rls_guard in the pool's connection customizer / on_acquire, so every pooled connection is guarded before first use. Bind the context per checkout, per query:
rust
// inside your pool's customizer:
unsafe {
let _g = LoadExtensionGuard::new(conn)?;
conn.load_extension("dist/rls.so", None)?;
}
conn.query_row("SELECT rls_protect(?1)", [POLICY_JSON], |r| r.get::<_, String>(0))?;
conn.query_row("SELECT rls_seal(?1)", [seal_token], |r| r.get::<_, i64>(0))?;
conn.query_row("SELECT rls_guard()", [], |r| r.get::<_, i64>(0))?;Notes
load_extensionisunsafebecause loading native code is inherently so;LoadExtensionGuardscopes the enable so application queries can't reload.- Pass claims as bound parameters (
?1) as shown — never format them into the SQL string. - A denied direct base-table read surfaces as a
rusqlite::ErrorwrappingSQLITE_AUTH; a write policy violation as aSQLITE_CONSTRAINTerror from the trigger'sRAISE(ABORT).