All writingAntvia · 6 min read

Access rules that live in the BI tool are not access rules

A row filter configured in a dashboard protects the dashboard. It stops protecting anything the moment a notebook

Apache SupersetSnowflakePostgressemantic layerApache IcebergGovernanceAnalytics and BIHealthcareFintechPublic sector

Row filters in a BI tool work exactly as advertised, right up until the day they do not. A clinician opens the patient dashboard and sees her own unit. A regional analyst opens the same dashboard and sees the sites she covers. Somebody from compliance sits in on a demonstration, watches two people log in and get two different numbers, and signs it off. That is a real control, and for the population of people who only ever touch the dashboard, it is doing real work.

Then the research team asks for a notebook connection to the same warehouse. Or finance wants the figures landing in a spreadsheet every Monday. Or a second BI tool arrives with an acquisition and nobody wants to migrate the reports this quarter. Each of those gets a database credential, because a credential is what you need in order to run a query. None of them goes anywhere near the row filter, because the row filter is a piece of configuration inside an application that is no longer in the path.

The uncomfortable way to say this is that the filter was never protecting the data. It was protecting the report. Those are two different objects with two different sets of ways in, and only one of them is what an auditor will ask you about.

The filter is a WHERE clause, and only one program writes it

It helps to be concrete about the mechanism. Most BI tools implement row-level security by holding a rule against a user or a group, and appending a predicate to the SQL they generate. The user asks for revenue by month, the tool builds the query, and before it sends it, it adds a condition restricting the rows to the ones that user is allowed to see. Column masking often works the same way, by rewriting the projected column into a case expression or a hash. The enforcement is real. It also lives entirely inside the query generator, which means the edge of the control is the edge of the generator, and anything that reaches the database by another route reaches it unfiltered.

  • The SQL editor in the same tool Most BI platforms ship a raw query interface. If a user has that privilege, their hand-written SQL is usually not rewritten, because the tool has no model of what the query means.
  • Cached and materialised datasets An extract built by an administrator holds the administrator's view of the data. Everyone who reads the cache reads that view.
  • Alerts and scheduled reports These commonly run under the owner's identity and post the result into a channel or an inbox with a different audience.
  • Transformation jobs dbt runs, reverse ETL into a CRM, and any pipeline that reads the same tables and writes them somewhere with its own permissions.
  • Anything speaking JDBC or ODBC Notebooks, spreadsheets, desktop tools, an engineer with a database client open on the second monitor.
  • Copies Replicas, restores, a dump handed to an implementation partner, a lower environment refreshed from production last Tuesday.

The identity problem underneath it

There is a reason the rules ended up in the BI layer, and it is not laziness. Most BI deployments connect to the warehouse with one service account. Fifty analysts log into the dashboard, and the database sees fifty queries from the same user. The BI tool knows who is asking. The database does not. If that is the shape of your connection, the BI tool is the only component with enough information to apply a per-person rule, so of course that is where the rule goes.

Moving enforcement down means fixing identity first. Broadly there are two routes. Either each user's own credential reaches the database, usually through OAuth token pass-through or key pair authentication per user, and the engine evaluates policy against the authenticated principal. Or the connection sets a session context before running the query, so the policy reads the current user from a session variable or a role that was assumed for the duration. Both work. The first is cleaner and the second is often what you can actually get running this quarter.

Top band: fifty analysts feed into a BI tool, which applies the row filter and then connects to the warehouse through one shared credential, so the warehouse sees fifty queries from a single principal. Bottom band: two panels. Route A gives each analyst their own credential straight to the warehouse, so policy sees each person. Route B keeps a pooled connection but sets who is asking before each query.
The reason the rule ended up in the BI tool. Only that layer knows who is asking, so either the person's identity has to reach the database or the connection has to carry it.
Two side by side topologies. On the left, three clients (dashboard, notebook, spreadsheet) all connect to a warehouse, with a filter icon sitting only on the dashboard connection and the other two arrows reaching the tables unfiltered. On the right, the same three clients connect through a policy boundary drawn around the warehouse itself, with the filter icon on the tables so all three arrows pass through it.
The same three clients, before and after the rule moves. The question is not how many tools you have, it is how many places the rule is written.

What enforcement at the query looks like

Every serious engine has a version of this. Postgres has row-level security policies attached to a table, evaluated for any client that touches it, with the caveat that the table owner and any role holding BYPASSRLS are exempt unless you turn on forced row-level security. Snowflake has row access policies and dynamic masking policies bound to the object rather than the consumer, so Snowsight, a JDBC driver and a Spark job all get the same treatment. On a lakehouse, the object store itself has no idea what a row is, so the enforcement point moves up into the catalog and the query engine, which is why the choice of catalog matters more than the choice of file format for this particular problem.

  1. 01
    Write down the predicate in wordsA user may read a row when the row's site is in the set of sites assigned to that user. If you cannot write the sentence, no syntax will save you.
  2. 02
    Get the entitlement into a tableA mapping of principal to permitted values, owned by whoever owns the joiner and leaver process, refreshed on a schedule you can name. This table is now a governed asset and needs its own tests.
  3. 03
    Bind the policy to the objectOn the table or the view, not on the report. Mask columns at the same time, and decide explicitly whether a masked column may still be grouped by or joined on.
  4. 04
    Prove it from outside the dashboardConnect as three personas with a plain SQL client and assert row counts. Run it in CI. A policy nobody re-tests after the next schema change is a policy that quietly stops matching.
The order matters. Most of the difficulty is in step two, and most teams discover that only after buying the feature in step three.

The edge of the control is the edge of whatever program writes the SQL.

The pattern, stated plainly

The parts that still leak

  • Caches keyed without identity If the BI cache key is the query text and the query text no longer carries the filter, two users share a result. Include the principal in the key, or do not cache filtered results at all.
  • Aggregates over restricted rows A user blocked from seeing individual salaries can often still see the average of a group of two. Small cell suppression is a separate control and the row policy will not do it for you.
  • Existence disclosure A filtered join that drops rows silently tells a careful observer that rows exist. So does a count that changes when a filter is applied.
  • Everything downstream of an export Once a permitted user downloads a CSV, policy has left the building. That is an argument for logging exports, not for pretending it does not happen.
  • Metadata Table names, column names and comments are often readable to anyone who can connect, and in a healthcare or investigations context the schema alone can be sensitive.

Where the query layer is the wrong place

It is worth saying plainly that this is not free and is not always right. Policies evaluated per row cost something, and a policy that calls a lookup function on a wide scan will show up in your query times. They are also close to invisible: an analyst gets a number that does not reconcile with a colleague's, and there is nothing in the dashboard to explain why, so the first hour of every investigation is spent rediscovering that a policy exists. Write the policy catalogue down somewhere a human reads, and make the tool say when a result is restricted.

There are also cases where none of this is the right answer. If every analyst is entitled to every row, do not build the machinery, because you will pay for it forever and it will protect nothing. If the true requirement is that a regulator's data never sits on the same disk as another tenant's, a predicate is the wrong instrument and physical separation is the right one. And if the concern is copies rather than queries, then export control, logging and contracts are where the effort belongs, because a perfect row policy does nothing about the spreadsheet that already left.

The audit question is not who can open the report

When this comes up in a review, the question that settles it is short. Take one restricted table and one restricted person, and ask what happens if that person connects to the warehouse with a plain SQL client and selects everything. If the answer is that they cannot connect, then the control is your credential process, and you should go and look at how many service accounts exist and who knows their secrets. If the answer is that they connect and get the filtered set, you have an access rule. If the answer is that nobody would do that, you do not have an access rule, you have a convention.

One test at the top: a restricted person connects to the warehouse with a plain SQL client and selects everything from a restricted table. Three answers below. They cannot connect at all means the control is the credential process. They connect and get the filtered set means a genuine access rule. Nobody would ever do that means a convention, marked with a cross.
Three answers, three different findings. Only the middle one survives the arrival of a second tool.

This is the reason we put governance on the query path in Antvia rather than in the reporting layer above it: the same masking and the same row filters apply whether the client is a dashboard, a notebook or somebody's ODBC connection, because the rule is attached to the table and not to the tool. It is a less exciting place to put a feature. It is the only place where the answer to the audit question stays true after the second tool arrives.

Find out what your row filters are actually protecting

If your access rules live in the BI tool, the fastest way to know where you stand is to test one restricted table from outside it. We can run that check with your team, map every route that reaches the warehouse today, and set out what moving policy onto the query would involve on your engine. Antvia is one way to get there, and it is not the only one worth considering.