skip to content

In Looker, how do access_filter and user attributes limit rows an explore returns?

level: seniorimportance: should knowfreq 46%

answer

  1. the restriction is per signed-in user
  2. a value stored per user drives it
  3. it is added to every generated query
  4. the model adds it, not the database

basics

~20 s

An access_filter declared on a Looker explore maps a field to a user attribute, so every query that explore generates gets a WHERE clause built from the signed-in user's attribute value. It is query-time filtering by the model layer, not database-level security.

solid answer

~40 s

You define a **user attribute** in Looker's admin (say `region`), give each user or group a value, then declare on the explore: ``` access_filter: { field: users.region user_attribute: region } ``` Every query from that explore now carries a filter on `users.region` using the current user's value, and users cannot remove it from the UI. For a constraint that is the same for everyone, `sql_always_where:` is the blunter tool; for hiding whole fields, `access_grant:` plus `required_access_grants:` gates them on an attribute value. The critical caveat is scope: these are model-layer filters. A user with SQL Runner access to the connection queries the database directly and bypasses them entirely, and a persistent derived table is built once with all rows regardless of who queries it later.

code

lookml · 13 lines
lookml
explore: orders {
  join: users {
    relationship: many_to_one
    sql_on: ${orders.user_id} = ${users.id} ;;
  }

  access_filter: {
    field: users.region
    user_attribute: region
  }

  sql_always_where: ${orders.is_test} = false ;;
}

go deeper

for a junior

Know that Looker can show different rows to different users, driven by a value stored on the user rather than by a filter the user picks in the UI.

for a middle

Explain the wiring: a user attribute, an access_filter on the explore binding a field to it, and the resulting WHERE clause on every query including scheduled deliveries.

for a senior

Show the limits — SQL Runner and develop access bypass the model layer, PDTs materialize unfiltered rows, and the empty-attribute case must be verified and made to fail closed.

for a principal

Own where the trust boundary sits: whether restriction belongs in LookML at all versus database row-level security or per-user credentials, and how that choice constrains caching, persistence and connection strategy.

## The three pieces Row restriction in Looker is assembled from three separate things, and candidates usually know one of them. **User attributes** are key-value pairs Looker stores per user, defaulted per group, and optionally populated from SSO/SAML at login. `region = "EMEA"`, `brand_id = "7"`. They are just values; on their own they restrict nothing. **`access_filter`** is a parameter on an explore that binds a field to one of those attributes: ``` explore: orders { access_filter: { field: users.region user_attribute: region } } ``` Every query generated from this explore — ad-hoc, saved Look, dashboard tile, schedule — gets a filter on `users.region` derived from the requesting user's attribute value. It is not visible as a removable filter in the UI, and scheduled deliveries run under the identity that owns the schedule. **`sql_always_where:`** is the unconditioned sibling: a raw SQL predicate appended to every query from the explore, e.g. `sql_always_where: ${orders.is_test} = false ;;`. It does not vary per user, and unlike a normal filter it cannot be seen or removed by the user. Related parameters `always_filter:` and `conditionally_filter:` set defaults a user *can* change, which makes them performance guardrails rather than security controls — do not confuse the two groups. ## Field-level restriction Rows are not the only thing worth hiding. `access_grant` declares a named grant tied to an attribute and its allowed values; fields, explores or joins then list `required_access_grants:`: ``` access_grant: can_view_pii { user_attribute: pii_access allowed_values: ["yes"] } dimension: email { sql: ${TABLE}.email ;; required_access_grants: [can_view_pii] } ``` Users without the grant do not see the field at all — it is absent from the field picker, and content referencing it errors rather than leaking. ## Where it breaks This is where senior answers separate themselves. **It is model-layer, not database-layer.** The filter is added when Looker composes SQL. Anyone who can issue SQL against the same connection outside the model bypasses it — most obviously SQL Runner, and equally a developer who can edit LookML to delete the `access_filter`. If your threat model includes those users, enforcement has to be in the database (per-user credentials, database row-level security, or a separate connection), not in LookML. **Persisted derived tables contain everything.** A PDT is built once, by Looker, and stored in a scratch schema on the connection. It is not built per user, so a PDT built from a restricted explore still materializes every row. The `access_filter` applies to the query *against* the PDT, which is usually fine — but it means the sensitive rows exist in a table other people may be able to read, and it means you cannot use persistence to "pre-filter" for a user. **An empty or missing attribute value is a policy decision.** Do not assume a user with no value set gets everything or gets nothing — verify the behaviour in your Looker version and set an explicit default that fails closed. This detail has shifted across versions and is exactly the kind of thing to test rather than recall. **Multi-value attributes.** Attributes can hold comma-separated lists for a user who legitimately spans several regions, and the filter honours Looker's filter syntax — including negation and wildcards typed into the attribute value. That is powerful and also a foot-gun: an attribute populated from an upstream system with unescaped input is an injection surface into the filter expression. Constrain the values you write. **Joins matter.** The filter applies to the named field, which lives in a view that must be joined into the query. If a user selects only fields from an unrelated view, Looker still adds the join needed to enforce the filter — which can change fan-out behaviour and cost. Check the generated SQL when you add an access filter to a wide explore. ## Testing it Looker lets an admin impersonate another user, which is the practical way to verify: sudo as a user in each group, run the explore, read the generated SQL, and confirm the WHERE clause carries the expected value. Do this for the empty-attribute case too. Reviewing the SQL is the only way to be sure the filter reached the query rather than being dropped by a join path or an override.

  • Does an access_filter restrict the rows stored in a persistent derived table?
    No. A PDT is built once by Looker with the full result set and stored in a scratch schema on the connection. The access filter is applied to queries run against the PDT, so the unfiltered rows still exist in that table.
  • When is an access_filter not sufficient as a security control?
    Whenever a user can reach the same connection outside the model — SQL Runner, a direct database client, or LookML develop permissions that let them edit the filter away. Real isolation needs database-side enforcement: per-user credentials, database row-level security, or separate connections.
  • How does sql_always_where differ from always_filter?
    `sql_always_where` appends a predicate the user cannot see or remove, so it is a hard constraint. `always_filter` only supplies a default filter value that the user is free to change, which makes it a convenience and performance guardrail rather than a restriction.
  • How would you verify an access filter actually works before shipping it?
    Impersonate a user from each group, run the explore, and read the generated SQL to confirm the WHERE clause carries that user's value. Repeat for a user whose attribute is unset, and confirm the empty case fails closed rather than returning everything.

saying these in an interview costs you the question

  • Claims access filters are enforced by the database
  • Assumes a blank user attribute safely returns no rows
  • Thinks a PDT is materialized per user
  • Confuses always_filter, which users can change, with a hard filter
  • Believes developers with LookML access are also restricted

context