Connect by JBRH Open Connect

Row-level security

Row-level security is PostgreSQL filtering rows by workspace on its own account, underneath everything the application does. The active workspace is published to the database on each request, so a query that never went through the object layer is confined anyway. It is the only layer that protects paths nobody remembered to protect, which is exactly why it is there.

Status
Available What this means
Audience
both
Last verified
Product version
6.3.2

What it protects that code cannot#

Application-level scoping protects the paths it knows about. That is a real protection and it covers the overwhelming majority of queries, but it has one structural weakness: it can only cover what goes through it. A raw statement written for speed, a maintenance script, a report, a new call site added in a hurry — each of those is outside the object layer by definition, and each would be unscoped if the object layer were the last word.

  • A query written directly against the database rather than through the object layer is still filtered.
  • A call site nobody remembered to scope is still filtered, because the filter is not at the call site.
  • A mistake in one screen cannot become a cross-business read, because the rows the database returns were already narrowed before the screen saw them.
  • The filtering is forced on every scoped table rather than advisory, so it is not something a connection can politely decline.

The technology itself, independent of Connect, is described in Row-level security as a database feature. This page is about the use Connect makes of it.

The write side, and the repair it refuses#

A policy has two halves. The read half decides which rows are visible; the write half decides which rows a statement is permitted to produce. The write condition is the one that surprises people, because it refuses things that look obviously correct.

The clearest case is a record whose workspace stamp is empty. It is invisible to every list, and the natural instinct is to write the correct stamp onto it from an ordinary request. The write condition refuses that: the statement would produce a row outside the workspace in force. The attempt turns a record that was merely invisible into an outright failure, which is worse.

How a table gets protected#

Creating a table and protecting it are two different acts. The application creates tables at boot; the policies come from an operational hardening step that reads the same list of scoped tables the object layer uses. A table that carries a workspace column but appears on neither list is protected by nothing at all — it is not half-protected, it is unprotected, and it will not announce itself.

  1. The table is added and listed as scoped, so the object layer filters and stamps it.
  2. The hardening step, reading that same list, applies the policy in the same release rather than a later one.
  3. The boundary check then covers it, because the check enumerates rather than assumes.

What it does not do#

It is not authorisation
It decides which rows a workspace may see, never whether a person may call a screen. That question is answered earlier, and separately — see Authorisation.
It does not narrow within a workspace
Every member of a workspace sees the workspace's rows. Finer distinctions are permissions, not row policies.
It does not cover a table nobody listed
The protection follows the list. This is the failure worth designing against, and it is why the list is code.
It is not the only layer
It is the last one. The customer surface and the object layer sit above it, and the reason there are three is that each absorbs a different single mistake.

Questions#

If the application already filters by workspace, why filter again?

Because the application can only filter what passes through it. The database filter applies to everything that reaches the data, including code written outside the object layer and code written after the reviewer stopped looking. Two independent filters do not fail to the same slip.

Why can a record with an empty workspace stamp not simply be fixed from a screen?

Because the write half of the policy refuses to produce a row outside the workspace in force, so the attempt fails rather than repairing anything. The repair runs as the schema owner at boot, where no request is involved and the condition does not apply.

Does this slow queries down?

It adds a condition to statements that were going to carry a workspace condition anyway, since the object layer applies its own. The measurable performance work in Connect has been about statement counts rather than about this filter — see Limits.