Contents
5. Security
Current Model
pg_liquid implements a graph-level read filter for Liquid facts and
compounds.
The executor reads the session principal from:
pg_liquid.policy_principal
If that setting is empty or unset, the executor does not apply Liquid-specific filtering and all non-deleted graph facts are readable.
If that setting is present, the executor only exposes data granted to that principal through graph edges and derived CLS policies.
First-Class Policy Model
The preferred authoring model is:
- bind the trusted request principal through
liquid.query_as(principal, program)orliquid.read_as(principal, program) - author explicit grants and derived policies as Liquid compounds
- keep
pg_liquid.policy_principalas the internal execution carrier, not the application-facing API
Primary explicit grant compounds:
ReadPredicate@(principal="...", predicate="...")ReadCompound@(principal="...", compound_type="...")ReadTriple@(principal="...", subject="...", predicate="...", object="...")
Primary derived policy compounds:
PredicateReadBySubject@(predicate="...", relation="...")PredicateReadByObject@(predicate="...", relation="...")CompoundReadByRole@(compound_type="...", role="...")
Principal inheritance is also supported:
Edge("<child-principal>", "liquid/acts_for", "<parent-principal>")
Optional metadata for authoring and introspection:
Principal@(id="...", kind="...")
The legacy built-in policy edges are still supported for compatibility:
Edge("<predicate>", "liquid/readable_if_subject_has", "<relation-predicate>")Edge("<predicate>", "liquid/readable_if_object_has", "<relation-predicate>")Edge("<compound-type>", "liquid/readable_compound_if_role_has", "<role-name>")
Examples:
PredicateReadBySubject@(predicate="name", relation="owner").
PredicateReadByObject@(predicate="member_of", relation="member").
CompoundReadByRole@(compound_type="Email", role="user").
Edge("person:alice", "owner", "user:alice").
Edge("org:acme", "member", "user:bob").
Email@(user="user:alice", domain="example.com").
With set pg_liquid.policy_principal = 'user:alice', the executor will allow:
Edge("person:alice", "name", "...")Email@(...)compounds whoseuserrole is"user:alice"
With set pg_liquid.policy_principal = 'user:bob', the executor will allow:
Edge(subject, "member_of", "org:acme")
If agent:support_bot acts for user:alice:
Edge("agent:support_bot", "liquid/acts_for", "user:alice").
the executor evaluates policy against the effective principal set
{agent:support_bot, user:alice}.
Legacy explicit grants are also still supported:
Edge("<principal>", "liquid/can_read_predicate", "<predicate>")Edge("<principal>", "liquid/can_read_compound", "<compound-type>")ReadTriple@(user="<principal>", subject="...", predicate="...", object="...")
Query Semantics
liquid.query(...) evaluates against all non-deleted rows in liquid.edges
that are visible to the current session principal.
The effective principal set is the bound principal plus every reachable
ancestor through liquid/acts_for.
For plain Edge(...) atoms, a fact is visible when at least one of these holds:
- the session has no
pg_liquid.policy_principal - the session principal has a matching
ReadPredicategrant - the session principal has a matching predicate grant
- the session principal has a matching exact
ReadTriplegrant - the predicate is granted through
PredicateReadBySubject - the predicate is marked
liquid/readable_if_subject_hasand the subject has the required relation edge to the principal - the predicate is granted through
PredicateReadByObject - the predicate is marked
liquid/readable_if_object_hasand the object has the required relation edge to the principal
For compound atoms, the executor allows a match when:
- the session has no
pg_liquid.policy_principal - the session principal has an explicit
ReadCompoundgrant - the session principal has an explicit compound-type grant
- the compound type is granted through
CompoundReadByRole - the compound type is marked
liquid/readable_compound_if_role_hasand the compound has the required role edge to the principal - all role edges needed to materialize that compound are individually visible
PostgreSQL Boundary
This CLS layer is an extension-level read filter. It does not replace ordinary PostgreSQL controls such as:
- ownership of the
liquidschema - function execution privileges
- table ACLs
Applications still need normal PostgreSQL permissions to prevent direct reads
from bypassing liquid.query(...). For least-privilege read-only access, prefer
granting only EXECUTE on liquid.read_as(...).
Trust Boundary Notes
pg_liquid.policy_principal is caller-supplied session context, not an
authenticated identity boundary on its own.
If a direct SQL client can SET or RESET pg_liquid.policy_principal, it can
impersonate another principal or disable CLS entirely for liquid.query(...).
For AI or end-user scoped access, treat pg_liquid as a filtering engine behind
a trusted application boundary. Do not assume direct SQL access plus
pg_liquid.policy_principal is sufficient isolation by itself.
liquid.read_as(...) is the supported least-privilege read surface for direct
SQL roles that should not read internal liquid tables or execute assertion-
capable Liquid programs. It runs as a security-definer wrapper and rejects
top-level assertions.
Operational rollout guidance, including recommended GRANT/REVOKE patterns,
is in 08_OPERATIONS.md.