Row access policies and the sharing boundary
A policy that filters on the current user behaves differently once the data is reached through a share. Confirm which execution context applies on every access path before relying on the policy to separate tenants.
Row-level security in Snowflake is straightforward to set up. Attach a row access policy to a table, have the policy read a mapping table of users and permitted values, and every query against that table is filtered automatically. For a single account with named users, that description holds.
Two things change that, and both are worth understanding before a policy is relied on to separate one tenant's data from another's.
The two execution contexts
Snowflake objects run in one of two rights contexts, and the difference determines which identity the policy is evaluated against.
Under caller's rights, the statement executes with the identity and roles of whoever invoked it. Session functions such as CURRENT_USER and CURRENT_ROLE return the caller, so a policy that reads those functions resolves to the person running the query and filters as intended. Ordinary queries against tables and views run this way.
Under owner's rights, the statement executes with the privileges of the object owner. An owner's rights stored procedure can read a table the caller holds no grant on, which is often the reason for writing one. It also means the identity a policy resolves to depends on the object it is evaluated inside, so the same policy can filter differently on two access paths.
A policy written against the session identity is therefore only as reliable as the context it is evaluated in. Before depending on one, confirm which context each access path uses, including the paths added after the policy was designed.
What happens across a share
Sharing across accounts removes the identity the policy depends on. When a consumer account queries shared data, the consumer's user identity does not exist in the provider account, so there is no user object for the policy to resolve. CURRENT_USER returns NULL in that session, and a policy that maps usernames to permitted values matches nothing.
Sharing is deliberately account-to-account, and the provider is not given the consumer's user directory. A permissive fallback branch in the same policy has the opposite effect and returns every row. Neither outcome raises an error. The problem is usually found by whoever builds the first report on the shared table.
The pattern that works across a share filters on the consuming account. CURRENT_ACCOUNT resolves correctly in a consumer session, so a mapping from account identifier to permitted tenant is a sound basis for a row access policy on shared data. Where separation is also needed between users inside a consumer account, combine the account-level filter with database roles granted through the share, so the consumer administers its own users while the provider keeps the tenant boundary.
Designing a tenant boundary that survives
In multi-tenant analytics, where one tenant must never see another tenant's rows, four decisions determine whether the boundary holds up over time.
Tenant identity. Decide which value identifies a tenant at query time, then test the policy from a normal user session, from any owner's rights object that exposes the data, and from a consumer account if the data is shared. Do not infer the result on one path from the result on another.
Attachment through tags. Attach policies with tags instead of to individual objects. A tag-driven policy is inherited by any new column or table that carries the classification, which removes the failure mode where a table added six months later is never protected.
Negative tests. Confirming that tenant A sees tenant A's rows is half of the test. The other half confirms that tenant A sees zero rows of tenant B, on every access path, and it belongs in CI where it runs on every change. Set USE SECONDARY ROLES NONE in the test session, because a session that inherits every secondary role granted to the user will pass a test the real user would fail.
The mapping table. The table that maps identities to permitted values is a security control. Give it the same change management, access restriction and audit as the policy itself, because anyone who can write to it can give themselves access to another tenant's rows.
Where this usually goes wrong
In the failures we are asked to look at, the policy logic is usually correct. The set of paths by which the data can be reached has grown since the policy was written, and the new path was never tested against it.
A view is created to serve a new dashboard, or a share is opened for a partner. A task or a stored procedure materializes a result into a new table, and that table carries no policy until one is attached to the copy. That last case is common enough that we check the materialized copies first.
One control catches all of these. Maintain a list of every path by which the protected data can be reached, keep it in version control alongside the policy definitions, and make adding a path require a test before the change is merged. The list takes maintenance. It is also the document an auditor asks for when they review how tenant separation is enforced.