Skip to main content

ABAC

ABAC — Attribute-Based Access Control — adds row-level security on top of RBAC. Where RBAC says "this user can read this table", ABAC says "this user can only read rows where country = 'ES'". The mechanism is tag-based: tables and columns get tags, policies bind tags to user attributes.

Two-layer defense

QRY's ABAC enforces in two layers, deliberately redundant:

Layer 1 — Instructional (LLM)

ABACPolicyEngine.build_llm_context() generates plain-text policy instructions and injects them into the system prompt of every conversation. The LLM is told "only generate SQL that filters on country = 'ES' for this user when querying tables tagged customer-data".

This catches most issues early: the LLM produces compliant SQL from the start.

Layer 2 — Enforcement (sqlglot validator)

The DatabaseTool.execute_query() pre-execution hook parses every SQL statement with sqlglot, matches tables to their tags, looks up applicable policies, and validates that the WHERE clause expresses the required filter. If the LLM forgot to add the filter (or worse, a malicious user injected SQL via a prompt), the query is blocked at execution time.

This is the safety net. Even if Layer 1 is bypassed, Layer 2 catches it.

The validator uses sqlglot dialects, so Postgres, BigQuery, Snowflake, etc. SQL all parse correctly. Target validation latency is under 50 ms.

Modes

The enforcement mode is per policy:

  • enforce — both layers active. Non-compliant queries are blocked.
  • audit_only — both layers active, but Layer 2 logs violations instead of blocking. Useful for rollout: write the policy, watch the audit log, switch to enforce when confident.
  • disabled — neither layer active. Policy stays in DB but is inert.

Tag matching

Policies match tags in two modes:

  • all (AND) — table must have every listed tag for the policy to apply.
  • any (OR) — table needs at least one listed tag.

Use all for narrow scopes (e.g. "only tables tagged both customer and pii"); any for broad ones ("any table tagged gdpr-restricted OR confidential").

Priority

Multiple policies can match the same table. Resolution: lower priority number wins. So a table-specific policy with priority=10 overrides a tag-wide policy with priority=100.

Where to configure

Admin > Access Control > ABAC. The UI has three tabs:

Tags

Define tags and apply them to tables / columns. Tags are free-form strings (pii, gdpr, customer, Z-table, etc.) — convention, not enforced.

Policies

For each policy:

  • Subject — which user attributes this applies to (e.g. role, group, country).
  • Tag selectorall / any plus the tag list.
  • Filter expression — the SQL WHERE fragment that gets enforced.
  • Modeenforce / audit_only / disabled.
  • Priority — number; lower wins.

Example: "users in the EU group can only see rows where region = 'EMEA' in any table tagged customer-data".

Audit

Every Layer 2 enforcement decision is logged with: timestamp, user, table, parsed SQL, applicable policy, decision. Use this to diagnose denials and to prove compliance.

Fail-open on validator errors

The Layer 2 validator can fail itself — sqlglot can't parse exotic SQL, an internal exception occurs, etc. In that case QRY fails open: it logs the validator error and lets the query through, relying on Layer 1's instructional control. This is deliberate — failing closed on a parser bug would block all queries.

For high-sensitivity tenants you can flip this to fail-closed via a feature flag. The default is "log loudly, don't block".

Multi-database support

Each datasource has a sqlglot dialect (Postgres, BigQuery, Snowflake, Trino, ...) so the validator parses correctly. SAP HANA is the trickiest — it uses non-standard SQL constructs that sqlglot occasionally can't parse. Fail-open kicks in when that happens.

Common issues

Policy added in enforce mode but the user can still see restricted rows. Layer 1 reaches the LLM, but you need to also restart the LLM service so the new policies are picked up — context generation is cached. Layer 2 picks up new policies on the next query without restart.

Policy is audit_only but I see no entries in the audit log. The user hasn't run a query that triggers the policy yet. Or the table they queried doesn't have the matching tag.

Layer 2 blocks a query that I think is compliant. Look at the audit log entry — it shows what sqlglot parsed and what policy fired. The most common cause: the user's WHERE clause is structurally different from the expected filter (e.g. country IN ('ES') vs country = 'ES'). Refine the policy's expected filter.

Validator latency exceeds 50 ms target. sqlglot is generally fast, but pathological queries (deeply nested subqueries on huge schemas) can be slow. Cache sqlglot parses per query hash if your tenant has heavy repetition.

Two policies match a table; the wrong one wins. Priority numbers — the lower one wins. Re-number priorities to put the more specific policy first.

See also

QRYA product of IXEN.