Implementing Deterministic Data Access Control with Colrows
BI-layer filters execute after your warehouse has already read and processed the data. The damaged is done before the policy fires. Colrows moves authorization to compile time, before any query reaches storage. Personas, scopes, and policies shape an allowed semantic subgraph during query planning. If a request falls outside that subgraph, compilation fails. The data is never touched.
The problem: BI-layer filters are structurally too late
Traditional architectures mask data after the query runs. The warehouse reads the full row. The BI tool applies the masking view. The audit log records that the raw data was retrieved. This is the confused-deputy problem: the warehouse has no idea who the real user is. It only knows the service account that connected. Any agent or SQL client that bypasses the BI tool sees all rows. The masking layer is optional, not structural.
Colrows takes the opposite approach. Policy is part of the semantic graph. During query planning, the requester's persona resolves an allowed subgraph. Compilation occurs entirely within it. If a metric depends on a node outside that subgraph, resolution fails. There is no query to run.
Compile-time, not after-the-fact
In a traditional architecture, a query runs against the raw schema and a row-mask or column-redaction layer trims the result. The data is already touched, the audit trail begins after the fact, and the controls are only as good as the masking layer's coverage. Colrows takes a different approach: policy is part of the graph. During semantic binding, the requester's persona resolves an allowed subgraph; compilation occurs entirely within it. If a metric depends on a node outside that subgraph, resolution fails. There is no query to run.
How Colrows differs
| Dimension | BI-Layer Filters | Colrows Compile-Time |
|---|---|---|
| Execution point | Runtime (post-read) | Compile time (pre-query) |
| Auditability | Weak; log-dependent | Deterministic; full trace |
| SQL performance | High latency (filter after) | Optimized (predicate pushdown) |
| Agent ready | No (bypassed by direct query) | Yes (all query paths) |
The pieces
| Object | What it does |
|---|---|
| Persona | A first-class graph node representing a role. Holds policy bindings and scope. |
| Scope | The slice of the graph a request is allowed to traverse - global / datastore / persona / user. |
| Policy | A formal predicate attached to nodes - RBAC, ABAC, row-level, column-level, time-window, region. |
| Permission | The smallest unit: who, on what, can do what. Three flavors: fixed, regex, class. |
| Redaction policy | A policy that hashes, masks, or drops a column for a persona. Applied during plan generation. |
Fixed permissions
The simplest case. A persona is granted explicit access to a named dataset, column, or metric.
persona: regional_analyst
grant:
- datasets: [analytics.orders, analytics.refunds]
- metrics: [net_revenue, gross_margin]
- dimensions: [region, product_category]
Regex permissions
Pattern-based grants for catalogs whose object lists change frequently.
persona: bi_team
grant:
- datasets: ["analytics\\.fact_.*"] # every fact_* table
- columns: [".*"] # all columns
deny:
- columns: [".*pii_.*", ".*ssn.*", ".*aadhaar.*"]
Deny rules always win. Regex matches are evaluated against the fully qualified name of the node in the semantic graph.
Class permissions
Classification-based grants. Tag nodes with classes (pii, financial, regulated) and write policies against the classes - not against table names that drift.
class: pii # tag nodes with this class
applies_to:
- dataset.column where tag = "personal_data"
policy:
- persona: contractor → deny
- persona: support_l1 → redact (hash)
- persona: dpo → allow
Class permissions compose: a column tagged pii AND financial inherits the most restrictive policy for the requesting persona.
Sample policy definitions
Region scoping (ABAC)
policy: region_scope
applies_to: dataset.row
predicate: row.region = persona.region # ABAC
binds_to: [regional_analyst, regional_manager]
Time-window restriction
policy: rolling_90_days
applies_to: metric.net_revenue
predicate: time_window <= 90 days
binds_to: [auditor]
PII redaction for support tier 1
policy: pii_hash
applies_to: column where tag = "pii"
action: sha256(value)
binds_to: [support_l1]
Zero-trust data access
The combination of persona scope, class-based policy, and compile-time enforcement gives you zero-trust by default:
- Default deny. A persona starts with no access; every grant is explicit.
- No implicit relationships. Joins must be proven on the graph; a persona without access to one side cannot use it as a join key.
- Policy as code. Policy lives in version-controlled YAML in the graph and changes are versioned, attributed, and reversible.
- Audit trail by construction. Every executed query carries the persona, scope, policies evaluated, and the structural reasoning trace.
Masking trusts that every query runner remembered to apply the masking view, every BI tool refreshed the column list, and every ad-hoc analyst followed the policy. Compile-time governance assumes none of those things are true.
Configuration example
Here's how compile-time governance looks in practice. A persona defines role, scope, and access rules:
persona "sales_rep_us" {
scope: region = "US"
scopes: product_family IN ("Electronics", "Home")
access {
revenue_metric: granted
customer_list: granted (exclude: "email", "phone")
cost_basis: denied
}
}
During semantic binding, Colrows resolves this persona against your graph. The result: revenue is accessible, customer names are visible but contact details are redacted, and cost data simply doesn't exist in this persona's allowed subgraph. No query this user generates can escape these bounds. No masking layer needed. No secondary audit log required. Authorization is part of the query plan itself.
Frequently Asked Questions
How does Colrows achieve row-level security without warehouse-native views?
Row-level security in Colrows is a structural property of the semantic graph. During query planning, policies are compiled into SQL predicates before the query is generated. The resulting SQL includes the row-level WHERE clauses built-in, not applied afterward. This means the warehouse never needs a separate view or masking layer.
Can Colrows policies be audited for compliance?
Yes. Every compiled query carries a complete audit trace: persona, scope, policies evaluated, nodes resolved, and the final SQL generated. This trace is deterministic and point-in-time reproducible, making it ideal for compliance audits (SOC2, HIPAA, GDPR). The audit log shows exactly what was authorized and why.
What happens when a policy is violated during query planning?
Compilation fails immediately. No query is generated. No data is read. The failure reason is returned to the caller, making it clear what permission was denied and why. This fail-safe design is what makes compile-time enforcement a zero-trust approach.
Best practices
- Tag columns with semantic classes (
pii,financial,regulated) and write policies against classes, not names. - Keep personas thin - bind one role to one persona; layer scopes through ABAC predicates.
- Prefer redaction (hash, mask) over outright denial when downstream signals still need a stable join key.
- Review the audit trail weekly - the failures (compilation refused) are as important as the successes.
Learn more
For a deeper architectural perspective on why compile-time governance is necessary, see Data Authorization: Why Security Fails in the Semantic Layer. That post covers the confused-deputy problem in detail, contrasts it with traditional BI-layer approaches, and explains why deterministic execution is the only safe model for enterprise data.
These same policies bind to identity over every integration surface, including AI agents connected through the MCP integration - an agent's metadata:read or data:query scope resolves through the identical persona and predicate stack described above.
Access policies are enforced independently of who curates the underlying definitions. Editing a metric or business term in Entity Manager changes what a term means - it never grants access to data the requester's persona wasn't already authorized to see.