| → / Space | Next slide |
| ← | Previous slide |
| Home | First slide |
| End | Last slide |
| Swipe | Touch navigation |
Your AI platform answers questions from live data, but not every user should see every answer. Sales reps should only see their own region's pipeline. Partners should see aggregated metrics but never individual customer records. Auditors need read access to compliance tables but nothing else.
These rules must apply consistently whether the question comes from a Genie space, a Vector Search query, an agent workflow, or a direct SQL call. Unity Catalog is the single enforcement layer that makes this possible: row filters, column masks, attribute-based access control, all resolved from the caller's identity.
This deck shows you the four layers of data access control and how they compose.
Every securable object lives in a three-level namespace. Permissions propagate downward.
catalog.schema.table -- Example applied_ai_gov.sales.deals
Tables, views, functions, volumes, models, and connections all live inside this hierarchy.
| GRANT on catalog | Applies to all schemas and tables within |
| GRANT on schema | Applies to all tables within |
| GRANT on table | Applies to that table only |
Grants are additive. A GRANT at the catalog level gives access to everything below. There is no implicit access: every identity starts with zero privileges.
Explicit-only model: No access without a GRANT. Owners have full control. Metastore admins can manage all objects.
UC evaluates all four layers in sequence before returning any data. Each answers a different question.
WHERE can they access?
Workspace bindings limit which workspaces see specific catalogs. Isolates dev/staging/prod.
WHO can access WHAT?
GRANTs on securable objects. Hierarchical inheritance. Owners have full control.
WHAT data based on tags?
Governed tags + policy functions enforce access dynamically at scale. Define once, apply to thousands.
WHAT rows/columns?
Row filters, column masks, and dynamic views control visibility at query time per identity.
All four layers evaluate in sequence. The user receives fully governed data: workspace verified, privileges checked, ABAC policies enforced, rows filtered, columns masked, all actions audited.
Control which workspaces can see specific catalogs, external locations, and storage credentials.
| Use case | Restrict production data to production workspaces only |
| Scope | Catalogs, external locations, storage credentials |
| Override | Takes precedence over user-level permissions |
A user with SELECT on a table still cannot access it from a workspace that is not bound to the catalog.
Explicit permission model. No implicit access.
-- Grant read access GRANT SELECT ON TABLE catalog.schema.deals TO `east_sales`; -- Grant at schema level GRANT USE SCHEMA ON SCHEMA catalog.sales TO `all_sales`;
Hierarchical: catalog → schema → table. Owners have full control over their objects.
Row filters automatically restrict which rows a user sees. Applied at query time by the SQL engine.
A SQL function is bound to a table. UC injects the function as a WHERE clause on every query. The user never sees filtered rows and does not know they exist.
-- Create the filter function CREATE FUNCTION sales_filter(region STRING) RETURN is_member('admin') OR region = 'WEST' AND is_member('west_sales'); -- Apply to table ALTER TABLE deals SET ROW FILTER sales_filter ON (region);
| Silent | Filtered rows are invisible. No error, no indication they exist. |
| Automatic | Enforced by the SQL engine on every query. No app code changes. |
| Identity-aware | current_user() and is_member() resolve at query time. |
| Same query | Different users run the same SQL, see different rows. |
One filter per table. If you need multiple filter conditions, combine them in a single function using OR/AND logic.
Two identity functions, different propagation behavior. Choose based on your token pattern.
Returns the calling user's email from the active SQL token.
-- Filter: only see your own rows
WHERE owner = current_user()
| OBO | Returns the human's email. Propagates through Genie, Agent Bricks, MCP chains. |
| M2M | Returns the SP's application ID. |
| Best for | Per-user row ownership, individual audit trails. |
Returns TRUE if the executing identity belongs to a workspace group.
-- Filter: role-based access
WHERE is_member('west_sales')
AND region = 'WEST'
| M2M | Evaluates the SP's group membership. Works well when you control the SP. |
| OBO | Evaluates the human's groups. Works as expected. |
| Best for | Role-based filtering, federation patterns. |
OBO pattern: prefer current_user(), which propagates the human identity end-to-end. Federation pattern: use is_member(), where role-based SPs map to workspace groups.
Column masks transform sensitive values at query time. Users see the row but with masked column values.
A SQL function is bound to a specific column. UC replaces the raw value with the function's output on every query.
-- Create the mask function CREATE FUNCTION mask_ssn(ssn STRING) RETURN CASE WHEN is_member('admins') THEN ssn ELSE '***-**-' || RIGHT(ssn, 4) END; -- Apply to column ALTER TABLE customers ALTER COLUMN ssn SET MASK mask_ssn;
| Transformation | Does not hide rows. Replaces the column value with a masked version. |
| Per-column | Each column gets its own mask function. Multiple columns can be masked independently. |
| Chainable | Row filter + column mask can apply to the same table. Filter runs first, then mask. |
| Return type | Must match the original column type. STRING in, STRING out. |
Analyst sees
***-**-6789
Admin sees
123-45-6789
A critical distinction when column masks use is_member() in different token patterns.
When a service principal executes the SQL directly, is_member() evaluates the SP's workspace group membership. You control group assignment explicitly.
-- SP is in 'admins' group is_member('admins') => TRUE -- Full SSN returned
In Genie or Agent Bricks OBO flows, is_member() evaluates the service layer's identity, not the calling user's groups. This is a known behavior.
-- Workaround: use allowlist table
CASE
WHEN EXISTS (
SELECT 1 FROM role_allowlist
WHERE email = current_user()
AND role = 'admin'
) THEN ssn
ELSE '***-**-' || RIGHT(ssn, 4)
END
Rule of thumb: For masks in OBO chains, replace is_member() with a current_user() lookup against a UC-governed allowlist table.
Attribute-Based Access Control uses tags to enforce dynamic, scalable policies without per-object permissions.
Account-level tags with enforced naming rules. Applied to catalogs, schemas, tables, or columns. Tags inherit downward.
-- Tag a table ALTER TABLE customer_data SET TAGS ('sensitivity' = 'high', 'region' = 'EMEA'); -- Tags alone don't enforce access -- They classify data for policies
Policies reference tags to dynamically control access. One policy can govern thousands of tables.
-- Policy: high-sensitivity tables -- require compliance-team access CREATE ACCESS POLICY high_sens_policy ON TABLE WHEN TAG 'sensitivity' = 'high' RESTRICT TO GROUP 'compliance_team';
Tags classify. Policies enforce. Change a tag and access changes instantly. No need to update permissions on individual objects.
| 1,000 tables | 1,000 GRANT statements per role. Manual, error-prone. |
| New table | Must remember to add GRANTs. Miss one and data leaks or access breaks. |
| Policy change | Update hundreds of GRANTs. Hope you got them all. |
| Audit | Piecing together who has access requires scanning every object. |
| 1,000 tables | 1 policy + tags. Applied automatically. |
| New table | Tag it. Policy applies instantly. |
| Policy change | Update one policy definition. Instant propagation. |
| Audit | Query tags and policies. Full visibility. |
One policy covers thousands of tables.
Re-tag a table to change access instantly.
Tags + policies = full governance lineage.
Both can apply to the same table. The row filter runs first, then column masks apply to surviving rows.
-- Row filter: region-based access CREATE FUNCTION deals_row_filter(region STRING) RETURN is_member('admin') OR (region = 'WEST' AND is_member('west_sales')) OR (region = 'EAST' AND is_member('east_sales')); ALTER TABLE deals SET ROW FILTER deals_row_filter ON (region); -- Column mask: hide margin from non-finance CREATE FUNCTION mask_margin(val DOUBLE) RETURN CASE WHEN is_member('finance') THEN val ELSE NULL END; ALTER TABLE deals ALTER COLUMN margin_pct SET MASK mask_margin;
| Deal | Region | Amount | Margin |
|---|---|---|---|
| Acme Corp | WEST | $50K | NULL |
| Beta Inc | WEST | $30K | NULL |
EAST rows filtered out. Margin masked.
| Deal | Region | Amount | Margin |
|---|---|---|---|
| Acme Corp | WEST | $50K | 42% |
| Beta Inc | WEST | $30K | 38% |
| East Co | EAST | $45K | 51% |
All rows visible. Real margins shown.
current_user()is_member()is_member()Most production deployments combine multiple layers. Workspace bindings for environment isolation. GRANTs for baseline access. ABAC for scale. Row filters + column masks for fine-grained control.
Not application code. Not middleware. The SQL engine itself enforces every layer.
Applications run the same SQL. Governance is invisible to the app layer. Add a row filter and every query automatically respects it.
Row filters and column masks are injected by the engine. No SQL trick, no API shortcut, no SDK workaround can skip them. The only override is ownership or admin privilege.
Every query is logged with the identity that executed it, the governance policies that fired, and the data that was returned. System tables provide the full lineage.
Same table. Same query. Different identity. Different data.
This is the core principle of UC governance. It works identically for Genie, Agent Bricks, MCP servers, notebooks, dashboards, and any tool that executes SQL through a warehouse.
| 4 | Complementary access control layers, evaluated in sequence |
| 2 | Identity functions: current_user() for OBO, is_member() for federation |
| 1 | SQL engine enforces everything, with no application code changes |
| ∞ | ABAC scales to thousands of tables with governed tags + policies |
Workspace bindings + GRANTs + ABAC + Row Filters + Column Masks
Defense in depth. Every layer adds protection. All audited.