jed

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:

  • TablesSELECT, INSERT, UPDATE, DELETE.
  • FunctionsEXECUTE.

Three layers compose into the effective privilege for an operation on an object:

  1. 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.
  2. grant — extra privileges on one object, beyond the default.
  3. 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(())
}

What each statement needs

  • SELECT on every table a statement reads — its FROM/JOIN tables, subqueries, an INSERT … SELECT source, and the columns an UPDATE/DELETE reads in WHERE / RETURNING / an assignment.
  • INSERT / UPDATE / DELETE on the write target. A statement that both reads and writes needs both: UPDATE t … WHERE … needs UPDATE and SELECT; a bare INSERT INTO t VALUES … needs only INSERT.
  • EXECUTE on every named function it calls. Built-in operators (+, =, …) are never gated — they are pure and unavoidable. Revoking EXECUTE on uuidv4() or now() 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.

jed — an embeddable, strictly-typed SQL database.