Data roles and row-level security
Deny-by-default grants, metric access, and parameterized row-level security.
Updated
datahashi denies access by default. A consumer cannot query an entity, dimension, measure, or metric unless a Data Role grants it.
flowchart TD
Req["Bearer token<br/>principal agent-01 · role sales_eu"] --> G
subgraph G["Governance enforcement"]
direction TB
S1["1 · Catalog grants<br/>revenue granted: YES<br/>Customer.region granted: YES<br/>raw Order.amount in governed mode: NO"]
S2["2 · Row-level security<br/>inject WHERE customer.region = current_user.region<br/>claim resolves to 'EU'"]
S1 --> S2
end
G --> SQL["SQL with bound filter<br/>WHERE customer.region = $1 · args: EU"]
Policy and compile meet in two places:
- Grants decide whether a metric or dimension appears in the catalog.
- Row filters add parameterized predicates to the SQL. Values are never concatenated as text.
Studio seats vs data roles
Platform permissions and query entitlements are separate:
| Dimension | Studio seat (authoring_role) |
Data Role (policy_roles) |
|---|---|---|
| Scope | Console / Control Plane | Query engine / catalog |
| Values | owner, editor, viewer |
Defined in policy.yaml (for example finance, sales_emea) |
| Grants | Edit models, invite members, publish | Query specific metrics and entities |
| Applies to | Human users in the console | Humans, MCP credentials, and SQL credentials |
An owner in the console cannot query protected finance metrics unless their principal also holds the matching Data Role.
Declare data roles
You write data roles in the engine file policy.yaml. The console uses the same format. This file is not the policies: block of the Query Runner config. The runner config names physical tables. This file names model members.
roles:
sales_representative:
description: Access to core sales metrics and customer dimensions for EMEA
grants:
- kind: metric
target: revenue
- kind: metric
target: order_count
- kind: metric
target: avg_order_value
- kind: entity
target: Customer
dimensions: [id, name, region, plan_tier]
row_filter: "region = current_user.region"
- kind: entity
target: Order
dimensions: [id, customer_id, status, order_date]
measures: [revenue, order_count]
finance_auditor:
description: Global access to all entities and metrics
grants:
- kind: metric
target: "*"
- kind: entity
target: "*"
fields: ["*"]
Grant types
kind: metric. A named metric, or*for every metric. The metric is granted only when every entity its measures live on is also granted.kind: entity. Name columns underdimensionsandmeasures.fieldsaccepts only["*"], or you omit it. Any otherfieldslist is refused at boot.- Wildcard.
fields: ["*"], or nofields/dimensions/measureskeys, exposes every dimension and every measure of the entity.
Row-level security
A row_filter predicate limits the rows a principal can see.
- Bound parameters. Claim values are bound as parameters. They never enter the SQL text.
- Several roles. The engine unions grants. It ANDs row filters. A second role cannot widen the first role’s rows.
Row-filter forms
Exactly four forms are accepted. Anything else fails the request with 403. The filter is never skipped.
region = current_user.region(also!=and<>). The claim holds exactly one non-empty value.region in current_user.regions(alsonot in). Keywords are lowercase. An empty list means zero rows.status = 'active'(also!=and<>). The literal applies to every principal.deleted_at is null.is not nullis not supported.
The left side is a dimension of the granted entity. A static ENGINE_API_KEY carries no claims. A claim-bound filter then fails every query made with that key. Use a literal filter with a static key, or use a JWT.
Attribute sources
When the identity token cannot carry claims, datahashi can call an Attribute Source over HTTPS at request time. The service returns a claims dictionary for the subject. If the lookup fails, the query is rejected.