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 selector —
all/anyplus the tag list. - Filter expression — the SQL
WHEREfragment that gets enforced. - Mode —
enforce/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
- RBAC — coarse access control. ABAC complements it.
- SAP module integration — modules + ABAC tags = defense in depth.
- ABAC reference — full feature reference, including sqlglot internals.