Authorization
jed has no users, roles, or GRANT statements in SQL — authorization lives above the engine, on
the session. A host serving untrusted queries configures a privilege envelope and the engine
enforces it mechanically: any operation the envelope withholds fails with 42501 at name
resolution, before it runs.
This is the concrete form of jed’s “untrusted SQL is safe to run” guarantee — pair it with the resource limits (max_cost and lifetime_max_cost) and you can hand an
adversary a query surface.
The model
Two object kinds, each with the PostgreSQL privileges jed has a feature for:
- Tables —
SELECT,INSERT,UPDATE,DELETE. - Functions —
EXECUTE.
Three layers compose into the effective privilege for an operation on an object:
default_privileges— the table privileges granted to every table (the “all tables” default). The default is all four; set it to{SELECT}for a read-only session.grant— extra privileges on one object, beyond the default.revoke— privileges withheld from one object. Revoke always wins over a grant and the default, so denying is order-independent.
A separate allow_ddl flag (default on) gates all CREATE / DROP / ALTER.
use jed::{CreateOptions, Database, Privilege, PrivilegeSet, SessionOptions};
fn main() -> jed::Result<()> {
let mut db = Database::create(CreateOptions { path: Some("app.jed".into()), ..Default::default() })?;
db.execute("CREATE TABLE report (id i32 PRIMARY KEY, body text)", &[])?;
db.execute("INSERT INTO report VALUES (1, 'hello')", &[])?;
// Serve untrusted queries through a SESSION granted ONLY read access: default_privileges =
// {SELECT} (a read-only envelope) with DDL disabled. A session is a handle minted from the
// database that shares its committed state. The engine enforces the envelope at name resolution —
// any write or schema change resolves to 42501, with no in-database role catalog.
let mut untrusted = db.session(SessionOptions {
default_privileges: PrivilegeSet::EMPTY.with(Privilege::Select),
allow_ddl: false,
..SessionOptions::default()
});
untrusted.execute("SELECT body FROM report", &[])?; // ok
let denied = untrusted.execute("DELETE FROM report", &[]);
assert_eq!(denied.unwrap_err().code(), "42501"); // permission denied for table report
untrusted.close(); // release the session (and its reader pin)
// grant/revoke adjust one object at a time on a session's envelope, and revoke always wins. Revoke
// EXECUTE on a volatile function to pin a session's determinism — calls to it then fail 42501.
let mut locked = db.session(SessionOptions::default());
locked.revoke(PrivilegeSet::EMPTY.with(Privilege::Execute), "uuidv4");
assert_eq!(locked.execute("SELECT uuidv4()", &[]).unwrap_err().code(), "42501");
locked.close();
Ok(())
}
package main
import (
"context"
"fmt"
"log"
jed "github.com/jackc/jed/impl/go"
)
func main() {
db, err := jed.CreateDatabase(jed.CreateOptions{Path: "app.jed"})
if err != nil {
log.Fatal(err)
}
defer db.Close()
ctx := context.Background()
mustExec(db, "CREATE TABLE report (id i32 PRIMARY KEY, body text)")
mustExec(db, "INSERT INTO report VALUES (1, 'hello')")
// Serve untrusted queries through a SESSION granted ONLY read access: DefaultPrivileges =
// {SELECT} (a read-only envelope) with DDL disabled. A session is a handle minted from the
// database that shares its committed state. The engine enforces the envelope at name resolution —
// any write or schema change resolves to 42501, with no in-database role catalog.
readOnly := jed.PrivSetEmpty.With(jed.PrivSelect)
noDDL := false
untrusted := db.Session(jed.SessionOptions{DefaultPrivileges: &readOnly, AllowDDL: &noDDL})
defer untrusted.Close() // release the session (and its reader pin)
if _, err := untrusted.Exec(ctx, "SELECT body FROM report"); err != nil {
log.Fatal(err)
}
if _, err := untrusted.Exec(ctx, "DELETE FROM report"); err != nil {
fmt.Println("denied:", err) // 42501 permission denied for table report
}
// Grant/Revoke adjust one object at a time on a session's envelope, and revoke always wins. Revoke
// EXECUTE on a volatile function to pin a session's determinism — calls to it then fail 42501.
locked := db.Session(jed.SessionOptions{})
defer locked.Close()
locked.Revoke(jed.PrivSetEmpty.With(jed.PrivExecute), "uuidv4")
if _, err := locked.Exec(ctx, "SELECT uuidv4()"); err != nil {
fmt.Println("denied:", err) // 42501
}
}
func mustExec(db *jed.Database, sql string) {
if _, err := db.Exec(context.Background(), sql); err != nil {
log.Fatal(err)
}
}
import { createDatabase, PrivilegeSet } from 'jed-ts';
const db = createDatabase({ path: 'app.jed' });
db.execute('CREATE TABLE report (id i32 PRIMARY KEY, body text)');
db.execute("INSERT INTO report VALUES (1, 'hello')");
// Serve untrusted queries through a SESSION granted ONLY read access: defaultPrivileges = {select}
// (a read-only envelope) with DDL disabled. A session is a handle minted from the database that
// shares its committed state. The engine enforces the envelope at name resolution — any write or
// schema change resolves to 42501, with no in-database role catalog.
const untrusted = db.session({
defaultPrivileges: PrivilegeSet.empty().with('select'),
allowDdl: false
});
untrusted.execute('SELECT body FROM report'); // ok
try {
untrusted.execute('DELETE FROM report');
} catch (e) {
console.log('denied'); // 42501 permission denied for table report
}
untrusted.close(); // release the session (and its reader pin)
// grant/revoke adjust one object at a time on a session's envelope, and revoke always wins. Revoke
// EXECUTE on a volatile function to pin a session's determinism — calls to it then fail 42501.
const locked = db.session({});
locked.revoke(PrivilegeSet.empty().with('execute'), 'uuidv4');
try {
locked.execute('SELECT uuidv4()');
} catch (e) {
console.log('denied'); // 42501
}
locked.close();
db.close();
What each statement needs
SELECTon every table a statement reads — itsFROM/JOINtables, subqueries, anINSERT … SELECTsource, and the columns anUPDATE/DELETEreads inWHERE/RETURNING/ an assignment.INSERT/UPDATE/DELETEon the write target. A statement that both reads and writes needs both:UPDATE t … WHERE …needsUPDATEandSELECT; a bareINSERT INTO t VALUES …needs onlyINSERT.EXECUTEon every named function it calls. Built-in operators (+,=, …) are never gated — they are pure and unavoidable. RevokingEXECUTEonuuidv4()ornow()is the easy way to pin a session’s determinism.
Existence is checked first
A privilege is required only once a name resolves to a real object. Selecting from a table that
does not exist is 42P01 (undefined table) even under an empty envelope — authorization gates what
exists, it never reveals what doesn’t by turning a different error code.