Skip to content

Policies

Policies are the core enforcement mechanism in BetweenRows. They determine which rows a user sees, which columns are visible or masked, and which tables exist from the user's perspective. This page is the landing for the whole policy cluster — which type to reach for, the JSON shape every policy shares, how they compose, and the deny-wins invariant. Each type has its own detailed guide linked below.

→ For the philosophy behind these design decisions, see Policy Model.

Which policy type do I need?

I want to...UseGuide
Filter rows by user identity (tenant, department, region)row_filterRow Filters
Redact a column value (SSN → ***-**-1234, email → ***@domain.com)column_maskColumn Masks
Allowlist specific columns (only these columns are visible)column_allowColumn Allow & Deny
Remove specific columns from resultscolumn_denyColumn Allow & Deny
Hide an entire table from a user or roletable_denyTable Deny

When to mask vs. when to deny

  • Mask when the column must remain queryable (JOINs, WHERE, GROUP BY work against the masked value) but the raw value should not be visible. Example: SSN masked to last-4, email domain preserved.
  • Deny when the column should not exist at all from the user's perspective — not in query results, not in information_schema, not usable in expressions. Example: credit_card column removed entirely.

Rule of thumb

If the user needs to reference the column (even with redacted values), mask it. If the user should not know the column exists, deny it.

The five types at a glance

TypeIntentGrants access?Modifies data?
row_filterpermitNoYes (filters rows)
column_maskpermitNoYes (transforms value)
column_allowpermitYes (named columns only)No
column_denydenyRemoves named columnsNo
table_denydenyRemoves table from catalogNo

Deny types are evaluated before permit types. There is no separate effect field — the type implies the effect.

column_allow is the only type that grants access. In policy_required mode, a table with no column_allow is invisible regardless of what else is configured. See Access mode interaction.

Structural shape

Every policy has the same top-level JSON shape:

json
{
  "name": "string (unique)",
  "policy_type": "row_filter | column_mask | column_allow | column_deny | table_deny",
  "targets": [
    {
      "schemas": ["public", "raw_*"],
      "tables": ["orders"],
      "columns": ["ssn"]
    }
  ],
  "definition": { /* type-specific, see below */ },
  "is_enabled": true,
  "decision_function_id": null
}

Target fields by policy type

policy_typeschemastablescolumns
row_filterrequiredrequired— (not used)
column_maskrequiredrequiredrequired (exactly one)
column_allowrequiredrequiredrequired
column_denyrequiredrequiredrequired
table_denyrequiredrequired— (not used)

Definition by policy type

  • row_filterdefinition is required:

    json
    { "filter_expression": "org = {user.tenant}" }
  • column_maskdefinition is required:

    json
    { "mask_expression": "'***-**-' || RIGHT(ssn, 4)" }
  • column_allow, column_deny, table_deny — no definition field; it must be absent.

The API rejects policies with the wrong shape (e.g., column_deny with a definition field → 422).

How policies compose

Multiple policies of the same type

SituationResolution
Multiple row_filter on the same tableAND-combined — a row must pass all filters to be visible. Layering narrows results, never expands.
Multiple column_mask on the same columnLowest priority number wins (highest precedence). Use distinct priorities to avoid undefined ordering.
Multiple column_deny on the same columnUnion — if any deny policy matches, the column is removed.
Multiple column_allow on the same tableUnion — visible columns are the union of all allow policies.

Deny always wins

If any enabled policy denies access — from any role, any scope, any source — the deny is enforced. A column_deny on salary overrides a column_allow that includes salary. A table_deny hides the table even if a row_filter exists for it.

This invariant means you can layer permit-policies freely and reach for a deny as the final word.

Policy changes take effect immediately

When you create, edit, enable, disable, or reassign a policy, the change takes effect for all connected users on their next query — no reconnect needed. BetweenRows rebuilds each user's view of the schema in the background.

Priority and assignment

Priority numbers

Every policy assignment has a numeric priority (default: 100). Lower number = higher precedence. When the same policy could be assigned through multiple paths (user + role + all), BetweenRows deduplicates and keeps the lowest priority number.

PriorityUse case
0–49Override policies (e.g., admin bypass)
50–99High-priority restrictions
100Default
101+Low-priority fallbacks

Assignment scopes

ScopeTargetMeaning
allApplies to every user on the data source
roleA specific roleApplies to all members (direct + inherited)
userA specific userApplies to that one user only

At equal priority, user-specific beats role-specific beats all.

Access mode interaction

The data source's access_mode changes what happens when no policy matches:

  • policy_required (recommended for production): tables with no column_allow policy are invisible. column_allow is the only type that grants access. Without it, the table returns empty results and is hidden from information_schema.
  • open: tables are visible by default. Row filters, masks, and denies narrow the view, but no column_allow is needed.

DANGER

column_deny does not grant table access. In policy_required mode, creating a deny-only policy without a column_allow leaves the table invisible — the deny has nothing to deny because the table was never granted.

→ Full explanation: Data Sources → Access modes

Template variables in expressions

row_filter and column_mask expressions can reference user attributes like {user.tenant}. Values are substituted as typed SQL literals — injection-safe by construction. → Full reference: Template Expressions

Wildcard targets

Policy targets support glob patterns for schemas, tables, and columns:

PatternMatchesDoes not match
"*"everything
"public"public onlypublic2, private
"raw_*"raw_orders, raw_eventsorders_raw, orders
"*_pii"customers_pii, employees_piipii_customers, customers
"analytics_*"analytics_dev, analytics_prodpublic, raw_analytics

Both prefix globs (col_*) and suffix globs (*_col) are supported on the columns field. Patterns are case-sensitive.

Validation

The API validates policies at create/update time:

  • row_filterfilter_expression must be parseable as a DataFusion expression. Unsupported syntax returns 422.
  • column_maskmask_expression must be parseable and must not reference columns outside the target table. Target entries must specify exactly one column per entry.
  • column_allow / column_denycolumns array must be non-empty in every target entry.
  • column_deny / table_deny / column_allow — the definition field must be absent.
  • policy_type — must be one of the five enum values.
  • Version conflictsPUT /policies/{id} requires the current version; mismatch returns 409.

Detailed guides

See also

  • Policy Model — the philosophy: zero-trust, deny-wins, visibility-follows-access