At Bytebase we build the governance layer in front of Postgres, MySQL, SQL Server, Oracle, etc. Going engine by engine on "how do we authorize an agent," we see the same gap in the open-source duo, Postgres and MySQL. Below, I use Postgres as the example.
1. Delegation: who is this session for
Alice asks a support bot for her refund status; Postgres only ever sees agent_svc, never Alice. This is delegation, the on-behalf-of (OBO) pattern: the session needs to carry both who it's acting for and who's actually calling.
The workarounds are advisory, not enforced:
SET ROLEis reversible:RESET ROLEundoes it in one statement.- Session variables are writable by the very session they're meant to constrain.
SQL Server has EXECUTE AS, Oracle has proxy authentication. Postgres has none of it, and agent chains need delegation to be chainable, something neither SQL Server nor Oracle has solved either.
2. Attenuation: how much can this session do
Attenuation means deriving a strictly weaker credential from a stronger one, and only weaker, never back up.
Postgres has nothing like it:
SET ROLE, reversible, same as above.default_transaction_read_only, a GUC the client flips back off.ALTER ROLE ... SET statement_timeout, a default not a ceiling, resettable at will.
Attenuation composes with delegation: what you want is Alice's permissions ∩ read-only ∩ this table ∩ 15 minutes.
3. Immutable application context: where do delegation and attenuation get stored
Where does a session store principal = alice@corp.com, boundary = read-only, task = refund-42 such that a policy can trust it?
- Custom GUCs, writable by anyone who can run SQL.
- SQL comments (sqlcommenter), a string convention, unparsed and unprotected.
Oracle's application contexts are writable only by a trusted package, which is why its VPD can trust them. SQL Server's sp_set_session_context takes a @read_only = 1 flag that locks a key for the rest of the connection.
4. Tags, not comments: what the data means
COMMENT ON covers every object, but it's one unstructured blob: no keys, no namespaces, PII flag and business definition crammed into one string.
Postgres has nothing at the label layer. Snowflake's TAG is a schema object, and a masking policy binds to the tag, not to any one column:
CREATE MASKING POLICY mask_pii AS (val STRING) RETURNS STRING ->
CASE WHEN CURRENT_ROLE() IN ('ANALYST') THEN val ELSE '**MASKED**' END;
CREATE TAG sensitivity;
ALTER TAG sensitivity SET MASKING POLICY mask_pii;
ALTER TABLE customers MODIFY COLUMN ssn SET TAG sensitivity = 'pii';Tag ssn and it's masked, immediately, no per-column policy to write. Every future column tagged sensitivity = 'pii' inherits the same masking, no code change at all. That's enforcement, not documentation.
What it looks like assembled
Once the four primitives exist, they feed one policy engine:
BEGIN SESSION AS agent_svc
ON BEHALF OF 'alice@corp.com' -- delegation
ATTENUATE TO (READ ONLY, TABLES (orders), TTL '15m') -- attenuation
WITH IMMUTABLE CONTEXT (task = 'refund-42', -- application context
trace = '4bf92f35');And a policy trusts its own predicates:
CREATE POLICY no_pii_for_agents ON customers
AS RESTRICTIVE
USING (
NOT column_has_tag('sensitivity', 'pii') -- data-side context
OR session_principal() = 'alice@corp.com' -- session-side context
AND session_context('task') LIKE 'refund-%'
);Nothing in it evaluates over an input the client can forge.
The audit log inherits the same chain
pgaudit can only write down current_user. With the four primitives in place, the audit entry is the whole chain:
agent_svc ON BEHALF OF alice@corp.com
task=refund-42 boundary=read-only,orders,ttl:15m
SELECT ... FROM customers
→ blocked by policy no_pii_for_agentsWithout native engine support, we built Bytebase as a proxy in front of Postgres that augments this missing context, then acts on it: access control, data masking, approval flow, audit logging.