Snowflake access governance — every statement reviewed before it runs
Last updated
AccessFlow governs Snowflake through the same submit, analyse, approve, execute pipeline as any other engine — but Snowflake gets its own engine plugin rather than the pooled JDBC lane, because its connection and auth model does not fit host, port, username and password. Warehouse sessions are billed while a warehouse is resumed, so the shape of the connection is itself a governance decision.
- Family
- Cloud data warehouse
- Query language
- Snowflake SQL
- Runs as
- Engine plugin (SHA-256 pinned)
- Default port
443- Default SSL mode
REQUIRE- Install
- One-click from the connector catalog
How AccessFlow connects
It is an engine plugin rather than an in-process JDBC datasource: a standalone shaded JAR, pinned in the connector catalog by URL and SHA-256, downloaded on first use, hash-verified, and loaded into an isolated classloader. If the hash does not match, it does not load. In an air-gapped install the JAR can be pre-seeded into the driver cache and the runtime told never to reach the network.
The host is the account host, the database name is the Snowflake database, and the stored credential is either a password or an unencrypted PKCS#8 private-key PEM — detected by its header and used for the driver's key-pair JWT authentication. Encrypted PEMs are rejected with a specific message rather than failing obscurely. A full connection URL can be supplied instead, carrying warehouse, role and schema parameters.
There is no connection pool. Each request opens a short-lived connection and closes it. That is deliberate: warehouse sessions are billed while the warehouse is resumed, governance traffic is sparse rather than continuous, and per-request connections also rule out session-state leaking between two users' queries.
What AccessFlow understands
The parser is a keyword classifier over a tokenizer that understands Snowflake's quoting rules — single-quoted strings, double-quoted identifiers and $$…$$ blocks — plus comments. It accepts SELECT, INSERT, UPDATE, DELETE, MERGE and table, view and schema DDL including TRUNCATE.
Operations map onto the same query types the SQL engines use, so the per-user read, write and DDL capabilities, the routing-policy engine and the multi-stage approval chains apply completely unchanged.
What it refuses
Rejected with distinct HTTP 422 messages: CALL, EXECUTE IMMEDIATE, scripting blocks (BEGIN, DECLARE), PUT and GET, COPY INTO, USE, SHOW, DESCRIBE, GRANT and REVOKE, and procedure, function, task, stream, pipe and stage DDL.
Multi-statement input is refused, and so are user-supplied ? placeholders — binds are reserved for row security, so a statement cannot arrive with its own parameter markers and collide with the ones the rewriter is about to add.
Row-level security and masking
Matching policies are ANDed into the WHERE clause, or a WHERE is inserted before GROUP BY, HAVING, QUALIFY, ORDER BY or LIMIT, with values bound as positional parameters. CTEs, subqueries, joins, set operators, MERGE and multi-table shapes all fail closed rather than execute unfiltered, and an INSERT into a policied table is rejected outright.
A policy with an empty value list is a deny-all that the executor short-circuits without opening a connection at all — so a misconfigured policy costs nothing and exposes nothing. Masking is applied after fetch by the same masker every engine shares.
What the rewrite actually does
A policy restricting orders to the caller's region narrows the statement before it is compiled — and before a single byte is billed:
-- submitted SELECT id, email FROM orders WHERE status = 'OPEN'; -- executed (row-security policy: orders.region = the caller's region) SELECT id, email FROM orders WHERE (status = 'OPEN') AND orders.region = ?; -- ^ bound, never concatenated
The value is a positional bind. A deny-all policy never reaches Snowflake at all: the executor short-circuits to an empty result without opening a connection, so a misconfigured policy costs nothing to run.
Dry-run and the scan-byte estimate
Snowflake bills by bytes scanned, so the dry-run is a cost control as much as a correctness one. AccessFlow plans the governed statement — row security already spliced in — behind an EXPLAIN USING TABULAR prefix, compiled by Snowflake and never executed, and reads the result defensively by column label so a release changing the output degrades rather than errors.
The aggregate post-pruning bytes become an estimated scan size shown to the reviewer before they approve, and the per-operator rows become a plan tree with partition pruning visible. A deny-all short-circuits to a zero-row, zero-byte estimate with no Snowflake call at all. DDL reports itself unsupported rather than guessing.