Contextflo Blog

How to restrict BigQuery access using authorized views

Don't hand out access to your real tables. Create a views-only dataset with just the columns and rows you want exposed, authorize it on the source, and connect that instead.

March 1, 20266 min readVivek Sah

The fastest way to connect a tool to BigQuery is to hand it a service account with Data Viewer on the whole project. It is also the way you end up with an AI assistant one question away from your payroll table.

You do not have to choose between "connected" and "safe". BigQuery has a mechanism for exactly this: a dataset that contains nothing but views, where each view exposes only the columns and rows you are comfortable with. The consuming account can query those views and genuinely cannot reach the tables underneath, even though the views read from them.

This guide sets that up end to end. It takes about twenty minutes and the same pattern works whether you are connecting Contextflo, a BI tool, or a contractor.

The shape of it

  1. A service account that can run queries and nothing else
  2. A dedicated dataset containing only views
  3. Data Viewer on that dataset only
  4. Authorized views so the views can read the source tables the account cannot

Step 4 is the one people miss, and it is where the whole thing either clicks or does not.

Step 1: Set up a service account

You can either ask Contextflo support for our service account email and grant it access (no JSON key ever leaves your project), or create your own.

Option A: grant access to Contextflo's service account

In IAM & Admin → IAM → Grant Access, add this principal:

[email protected]

Give it the role BigQuery Job User. That is it at the project level. You will add Data Viewer on your views dataset in Step 3, then skip to Step 6 to connect. No key file to generate, store, or rotate, which is why most teams pick this one.

Option B: create your own service account

Go to IAM & Admin → Service Accounts → Create Service Account:

  • Name: contextflo-restricted
  • Description: Restricted read access via views only

Grant it BigQuery Job User at the project level only. Do not add Data Viewer here. That is the whole point: Job User lets it run queries, but it has nothing to run them against yet.

Then create a key: find the account in the list → ⋮ → Manage keysAdd key → Create new key → JSON.

{
  "type": "service_account",
  "project_id": "your-project-id",
  "private_key_id": "abc123...",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
  "client_email": "[email protected]",
  "client_id": "123456789...",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token"
}

Store the file in a secret manager, not a repo and not Slack. A leaked service account key is valid until somebody notices and revokes it, which is the argument for Option A.

Step 2: Create a views-only dataset

In the BigQuery console, click your project name → Create Dataset:

  • Dataset ID: restricted_views
  • Location: the same region as your source datasets (a view cannot cross regions)

Then create views that select exactly what you want exposed:

-- Example: expose non-PII order data only
CREATE VIEW `project.restricted_views.orders` AS
SELECT
  order_id,
  order_date,
  product_category,
  revenue
FROM `project.raw_data.orders`
WHERE order_date >= '2024-01-01'

This is where the actual decisions get made, and it is worth being deliberate rather than mechanically wrapping every table. Two things to decide per view:

Columns. Leave out email, phone, address, raw payment identifiers, anything you would have to report on if it leaked. If a column is useful but sensitive, consider exposing a derived form instead: domain rather than email, an age bucket rather than a date of birth.

Rows. The WHERE clause is a filter you get for free. Restricting to the last two years, or to non-test accounts, or to a single region, costs nothing and shrinks what any single mistake can expose.

Step 3: Grant dataset-level access

On the restricted_views dataset, go to Sharing → Permissions → Add Principal. Paste the service account email and assign BigQuery Data Viewer.

The account can now see the views, but querying one still fails, because the view reads tables the account has no access to. That is expected, and it is what Step 4 fixes.

Step 4: Authorize the views on the source datasets

The gotcha that costs everyone an hour: authorized views are configured on the source dataset (the one being read from), not on the dataset where the view lives.

Go to the dataset holding the source tables, for example raw_data, and click Sharing → Authorize Views. Enter the full view path, project.restricted_views.orders, and click Add Authorization.

Repeat for every source dataset the view touches. If a view joins across three datasets, it needs authorizing on all three.

What this does is grant the view permission to read the source, without granting anything to whoever queries the view. The service account never gains access to raw_data, and no amount of clever SQL will get it there.

Authorize the whole dataset instead

If you expect to keep adding views, authorize the dataset rather than each view. On the source dataset, Sharing → Authorize Datasets, add restricted_views. Every view in it, including ones you create next month, can then read from that source with no further clicks.

This is the version I would use in practice. Per-view authorization is precise, but it also means every new view silently fails until someone remembers this screen.

Step 5: Verify

Two queries. The first should return rows:

SELECT * FROM `project.restricted_views.orders` LIMIT 10

The second should fail with "Access Denied":

SELECT * FROM `project.raw_data.orders` LIMIT 10

Run both. A setup that only passes the first test is a setup you have not actually verified — the whole value here is in the second query failing, so confirm it does.

If the view query fails instead, a source dataset is missing its authorization. The error message names the table it could not read, which tells you which dataset to fix.

Step 6: Connect to Contextflo

Go to Settings → Data Sources → Add Data Source → BigQuery:

  • Connection Name: something recognisable, like "Analytics Views"
  • Project ID: your GCP project ID
  • Location: the dataset region, e.g. US or EU
  • Service Account JSON: upload the key (Option B only — skip if you used Option A)

Click Test Connection, then Connect. Contextflo discovers only the views in restricted_views, because that is genuinely all this account can see.

Chained views

If a view reads another view, every dataset in the chain needs authorizing:

restricted_views.summary
  → analytics.orders_enriched (view)
    → raw_data.orders (table)
    → raw_data.customers (table)

Authorize restricted_views.summary on analytics, and analytics.orders_enriched on raw_data.

Keep the hierarchy shallow. One level is easy to reason about and easy to hand to whoever inherits it. Three levels means that a year from now, nobody will be able to answer "what can this account actually read?" without tracing the whole graph.

What this does and does not cover

Worth being clear about the boundaries before you treat this as finished:

  • It is not per-user access control. Everyone connecting through this service account sees the same views. If marketing and finance need different tables, either build separate view datasets with separate accounts, or handle it in the layer above. Contextflo scopes tables per user and per group on top of whatever the connection can reach, which is usually the less painful path.
  • It is not dynamic row-level security. The WHERE clause is static. Filtering rows per requesting user needs BigQuery's row-level access policies on the underlying table, which is a different feature.
  • Views need maintenance. Add a column to a source table and it does not appear until someone updates the view. That is mostly a feature — nothing new is exposed by accident — but it means the views are now something you own.

Quick reference

WhereWhatWhy
Project levelBigQuery Job UserAllows running queries
Views datasetBigQuery Data ViewerCan see and query the views
Each source datasetAuthorized view or datasetLets the view read source tables

If a query is denied, the answer is almost always the third row.

Questions on any of this: [email protected].