Couchbase access governance — every SQL++ statement reviewed before it runs
Last updated
AccessFlow governs Couchbase through the same submit, analyse, approve, execute pipeline as a SQL database. Couchbase speaks SQL++ (N1QL), which is SQL-shaped, so the engine classifies statements with a purpose-built tokenizer and splices row-level security into the WHERE clause — with the shapes it cannot prove safe failing closed rather than running unfiltered.
- Family
- Document
- Query language
- SQL++ (N1QL)
- Runs as
- Engine plugin (SHA-256 pinned)
- Default port
11210- Default SSL mode
DISABLE- 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.
One native cluster client is cached per datasource. Connection strings are built from host and port, with the scheme chosen by SSL mode: plaintext when disabled, or TLS with trust-any-certificate, CA validation, or full SDK defaults as the mode tightens. A full URL can be supplied verbatim instead.
Scan consistency defaults to request-plus, meaning a read observes mutations submitted before it — the predictable choice for a governance proxy, where a reviewer approving a statement should not then see a stale result. The faster unbounded default is available if you would rather have it.
What AccessFlow understands
Exactly one statement per submission, with a trailing semicolon tolerated. The tokenizer strips comments and is aware of string and backtick literals and nesting depth. SELECT is a read; INSERT and UPSERT are inserts; UPDATE and MERGE are updates; DELETE is a delete; creating or dropping an index, scope or collection is DDL.
Statements execute in the bucket's default-scope query context, so a bare FROM users resolves inside the datasource bucket and is matched against a collection-level grant, while a fully qualified bucket, scope and collection path is carried through verbatim for the allow-list to check.
What it refuses
Three things fail closed with HTTP 422, and they are the SQL++ counterparts of the server-side-code ban every other engine has: the CURL() function, which would let a statement exfiltrate query results over the network from inside the database; JavaScript user-defined functions, in every form — creating, executing or dropping them; and the system: keyspaces.
Anything the classifier does not recognise is refused rather than passed through.
Row-level security and masking
Policies are ANDed into the WHERE clause of a simple single-keyspace read, update or delete, bound as named parameters and never concatenated; an existing WHERE expression is parenthesised first so operator precedence cannot widen it. An empty value list becomes a literal false — a deny-all, not a no-op.
The shapes the splice cannot provably filter fail closed: CTEs, subqueries, JOIN, NEST and UNNEST, USE KEYS, set operations, multi-keyspace statements, and MERGE — which is both a join-shaped DML and a carrier for an insert branch. An INSERT or UPSERT into a policied keyspace is rejected outright.
Results are materialised into the same tabular shape the SQL engines produce, with a SELECT * page unwrapped from its keyspace-alias wrapper so that masking rules written as collection.field still match, and masks applied per value by the shared masker.
What the rewrite actually does
A policy restricting the orders collection to the caller's region is ANDed into the WHERE clause, with the existing predicate parenthesised first so precedence cannot widen it:
-- 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 region = $af_rls_1; -- ^ named parameter
Had the statement been a join, an UNNEST, a USE KEYS lookup or a MERGE, it would have been refused with HTTP 422 — those are the shapes the splice cannot prove it has narrowed.
Introspection and dry-run
The connection test waits for the bucket and then runs a trivial query through the query service — proving SQL++ can actually execute, not merely that the key-value port answered. Introspection maps scopes to schemas and collections to tables, samples fields with a bounded read per collection, and reports the document key as the primary key, degrading to the key alone for a collection with no index.
A dry-run runs the governed statement through Couchbase's own EXPLAIN. On submission, an UPDATE or DELETE additionally gets an exact affected-document count from a SELECT COUNT(*) splice carrying the same row-security predicate.