KaelioDocs

Policy YAML Reference

Complete schema reference for SQL Security Policy YAML files, including all fields, glob patterns, conditions, and rule evaluation behavior.

This page documents every field available in a Kaelio SQL Security Policy. Policies are YAML documents that define table access, column filtering, and row-level security rules for a data source.

Policy Structure

A policy YAML file contains four top-level fields:

structure.yaml
version: "1.0"
default_allow_tables: true
table_rules: []
column_rules: []
row_filter_rules: []

All fields have defaults, so a completely empty document is a valid (permissive) policy.

Top-Level Fields

FieldTypeDefaultDescription
versionstring"1.0"Policy schema version. Currently only "1.0" is supported.
default_allow_tablesbooleantrueWhether tables not matched by any table rule are accessible.
table_rulesarray[]Rules that allow or block access to specific tables.
column_rulesarray[]Rules that hide columns from query results.
row_filter_rulesarray[]Rules that inject WHERE clauses to filter visible rows.

default_allow_tables

This field controls what happens when a query references a table that does not match any entry in table_rules. It sets the baseline posture of your policy.

Allowlist Mode (default_allow_tables: false)

All tables are blocked unless explicitly allowed. Use this for high-security environments where you want to enumerate exactly which tables users can query.

allowlist-mode.yaml
version: "1.0"
default_allow_tables: false

table_rules:
  - table_name: products
    allowed: true
  - table_name: categories
    allowed: true

In this example, only products and categories are queryable. Any other table reference causes the query to be rejected.

Blocklist Mode (default_allow_tables: true)

All tables are accessible unless explicitly blocked. Use this when most tables are safe to query and you only need to restrict a few.

blocklist-mode.yaml
version: "1.0"
default_allow_tables: true

table_rules:
  - table_name: audit_logs
    allowed: false
  - table_name: internal_config
    allowed: false

When you are unsure which mode to pick, start with blocklist mode (default_allow_tables: true). You can always switch to allowlist mode later as your requirements tighten.


Table Rules

Each entry in table_rules controls whether a table can be queried.

FieldTypeRequiredDescription
table_namestringYesTable name or glob pattern to match.
allowedbooleanYestrue to allow access, false to block it.
conditionobjectNoAttribute-based condition that must pass for this rule to apply.
table-rules-example.yaml
table_rules:
  - table_name: audit_logs
    allowed: false

  - table_name: analytics_*
    allowed: true
    condition:
      department: analytics

Column Rules

Each entry in column_rules specifies columns to hide from query results for a given table.

FieldTypeRequiredDescription
table_namestringYesTable name or glob pattern to match.
restricted_columnsarray of stringsYesColumn names to strip from SELECT queries.
conditionobjectNoAttribute-based condition that must pass for this rule to apply.
column-rules-example.yaml
column_rules:
  - table_name: users
    restricted_columns:
      - password_hash
      - ssn

  - table_name: employees
    restricted_columns:
      - salary
      - bank_account
    condition:
      role: ["hr", "admin"]

In the second rule, the salary and bank_account columns are only hidden from users whose role is hr or admin. For all other users, those columns are visible.

Column rules are cumulative. If multiple rules match the same table, all of their restricted_columns lists are merged. A column hidden by any matching rule is hidden regardless of what other rules say.


Row Filter Rules

Each entry in row_filter_rules injects a WHERE clause to restrict which rows a user can see.

FieldTypeRequiredDescription
table_namestringYesTable name or glob pattern to match.
filter_sqlstringYesSQL WHERE clause fragment. Supports {variable} template placeholders.
conditionobjectNoAttribute-based condition that must pass for this rule to apply.

Template variables in filter_sql are replaced with values from the querying user's security properties. Use {property_name} syntax.

row-filter-example.yaml
row_filter_rules:
  - table_name: orders
    filter_sql: "tenant_id = '{tenant_id}'"

  - table_name: documents
    filter_sql: "department = '{department}' AND classification != 'TOP_SECRET'"
    condition:
      role: viewer

When a user with tenant_id: acme queries the orders table, the filter becomes tenant_id = 'acme' and is appended as a WHERE clause.

If a template variable like {tenant_id} cannot be resolved from the user's security properties, the query is rejected entirely. Ensure that every user who queries a filtered table has the required properties set.


Glob Patterns

The table_name field in all rule types supports glob patterns for matching multiple tables with a single rule.

Supported Syntax

PatternMeaningExample Match
*Matches everything (all tables)Any table
analytics_*Matches any table starting with analytics_analytics_events, analytics_daily
*_logsMatches any table ending with _logsaccess_logs, error_logs
schema.*Matches any table in a schemaschema.users, schema.orders
?Matches a single characterlog_? matches log_a but not log_ab

Priority Order

When multiple rules match the same table, the most specific pattern takes priority. Rules are evaluated in the following order:

  1. Exact match -- orders is more specific than any pattern
  2. Specific patterns -- analytics_* is more specific than * because it has more literal characters
  3. Universal wildcard -- * has the lowest priority

Among patterns with the same specificity, the rule that appears first in the YAML list wins (for table rules and row filter rules).

glob-priority.yaml
table_rules:
  # Priority 3 (lowest): catch-all
  - table_name: "*"
    allowed: false

  # Priority 2: pattern match
  - table_name: "public_*"
    allowed: true

  # Priority 1 (highest): exact match
  - table_name: public_secrets
    allowed: false

In this example, public_reports is allowed (matches public_*), public_secrets is blocked (exact match overrides the pattern), and any other table is blocked (matches *).

Declaration order in the YAML does not affect priority. Exact matches always win over patterns, and more specific patterns always win over less specific ones, regardless of where they appear in the list.


Conditions

Conditions make a rule apply only when the querying user has specific security property values. Add a condition object to any rule to enable attribute-based access control.

Condition Fields

The condition object is a dictionary where:

  • Keys are user security property names (e.g., role, department, tenant_id)
  • Values are the required property values

Matching Modes

Exact match -- Use a string value. The user's property must equal this value exactly.

exact-match.yaml
condition:
  department: engineering

IN match -- Use an array of strings. The user's property must equal one of the listed values.

in-match.yaml
condition:
  role: ["admin", "manager", "analyst"]

Multiple keys (AND logic) -- When a condition has more than one key, all of them must match for the condition to pass.

and-logic.yaml
condition:
  department: engineering
  role: ["lead", "senior"]

This condition passes only for users who are in the engineering department AND whose role is either lead or senior.

Fail-Closed Behavior

Conditions are fail-closed: if the user does not have a property that a condition checks, the condition fails. For example, a condition of department: sales will fail for any user who has no department property set, even if all other conditions would pass.

Conditional Rule Example

conditional-example.yaml
table_rules:
  - table_name: financial_reports
    allowed: true
    condition:
      department: finance

  - table_name: financial_reports
    allowed: false

The first rule allows finance department users to access financial_reports. The second rule (no condition) blocks everyone else. Both rules have the same table_name, so they share the same specificity -- the first matching rule wins.


Rule Evaluation Order

Understanding how rules are evaluated helps you predict the outcome of any query.

Table Rules

  1. Find all table_rules entries whose table_name matches the queried table (exact or glob).
  2. Sort matches by specificity: exact match first, then specific patterns, then *.
  3. For each match (in order), check the condition (if any):
    • No condition: the rule applies immediately. Return allowed value.
    • Condition passes: the rule applies. Return allowed value.
    • Condition fails: skip this rule and try the next match.
  4. If no rule applies, fall back to default_allow_tables.

Result: First matching rule wins.

Column Rules

  1. Find all column_rules entries whose table_name matches the queried table.
  2. For each match, check the condition (if any):
    • No condition or condition passes: add restricted_columns to the restricted set.
    • Condition fails: skip this rule.
  3. The union of all restricted columns from passing rules is applied.

Result: All matching rules are cumulative.

Row Filter Rules

  1. Find all row_filter_rules entries whose table_name matches the queried table.
  2. Sort matches by specificity (same ordering as table rules).
  3. For each match (in order), check the condition (if any):
    • No condition or condition passes: apply filter_sql and stop.
    • Condition fails: skip this rule and try the next match.
  4. If no rule applies, no row filter is added.

Result: First matching rule wins.

When No Rules Match

Rule TypeBehavior When No Rule Matches
Table rulesdefault_allow_tables determines access
Column rulesAll columns are visible (no restrictions)
Row filter rulesNo WHERE clause is injected

Complete Example

The following policy demonstrates all features working together.

complete-policy.yaml
version: "1.0"
default_allow_tables: false

# --- Table access ---
table_rules:
  # Block all internal tables
  - table_name: "internal_*"
    allowed: false

  # Allow public-facing tables for everyone
  - table_name: products
    allowed: true
  - table_name: categories
    allowed: true

  # Allow order tables only for sales and support teams
  - table_name: orders
    allowed: true
    condition:
      department: ["sales", "support"]

  - table_name: order_items
    allowed: true
    condition:
      department: ["sales", "support"]

  # Allow all tables for admins (lowest priority due to wildcard)
  - table_name: "*"
    allowed: true
    condition:
      role: admin

# --- Column restrictions ---
column_rules:
  # Always hide authentication columns
  - table_name: users
    restricted_columns:
      - password_hash
      - mfa_secret
      - recovery_codes

  # Hide PII from non-compliance users
  - table_name: users
    restricted_columns:
      - ssn
      - date_of_birth
      - home_address
    condition:
      department: compliance

  # Hide pricing internals across all tables matching the pattern
  - table_name: "pricing_*"
    restricted_columns:
      - cost_basis
      - margin_pct

# --- Row-level filtering ---
row_filter_rules:
  # Tenant isolation on the orders table
  - table_name: orders
    filter_sql: "tenant_id = '{tenant_id}'"

  # Department-scoped access to documents
  - table_name: documents
    filter_sql: "department = '{department}'"
    condition:
      role: viewer

  # Admins see all documents (no filter)
  - table_name: documents
    filter_sql: "1 = 1"
    condition:
      role: admin

How This Policy Evaluates

Consider a user with these security properties:

department: sales
role: viewer
tenant_id: acme
QueryResult
SELECT * FROM productsAllowed. products is explicitly permitted for everyone.
SELECT * FROM internal_metricsBlocked. Matches internal_* which is set to allowed: false.
SELECT * FROM ordersAllowed with row filter. The user is in sales, so the table rule passes. Row filter adds WHERE tenant_id = 'acme'.
SELECT * FROM usersBlocked. No table rule matches users, and default_allow_tables is false.
SELECT * FROM documentsBlocked. No table rule matches documents, and default_allow_tables is false.

Now consider an admin user:

role: admin
tenant_id: acme
QueryResult
SELECT * FROM usersAllowed via the * wildcard table rule (admin condition passes). Columns password_hash, mfa_secret, and recovery_codes are still hidden.
SELECT * FROM documentsAllowed via wildcard. Row filter 1 = 1 is applied (admin condition on the first matching row filter rule passes), so all rows are visible.
SELECT * FROM ordersThe orders table rule requires department: ["sales", "support"]. The admin has no department property, so that condition fails. The * wildcard rule applies next, and the admin condition passes. Access is allowed. Row filter adds WHERE tenant_id = 'acme'.

On this page