13. Data Access Control
You'll learn how to protect sensitive columns at schema scale using Unity Catalog ABAC policies in ~30 min (plus a 55-min hands-on lab).
Prereqs: 5. Data Governance Strategy, Unity Catalog foundations, 3. Infra Setup: Add Groups
Starter Journey Progress
The right approach for column-level access control in Unity Catalog is ABAC (Attribute-Based Access Control). You define a masking policy once, attach it to a tag, and every column in every tagged table inherits the policy. No per-table wiring. No risk of a developer forgetting to add the mask when they create a new table.
Think of it like a building's badge system. You set the rule at the door, not at each desk. The right badge gets you in; everyone else is redirected at the entrance, regardless of how many new desks get added.
How ABAC works
Three building blocks:
- Tag: a label applied to a column that identifies what the policy should match
- Masking UDF: a SQL function that returns a substitute value
- Policy: one statement that binds the UDF to any column carrying that tag, anywhere in a schema or catalog
-- 1. Tag: label the column so a policy can find it
ALTER TABLE main.hr.employees ALTER COLUMN salary SET TAGS ('sensitivity' = 'high');
-- 2. UDF: returns a substitute value; who sees the real value is decided by the policy, not the function
CREATE OR REPLACE FUNCTION main.security.mask_salary(salary DOUBLE)
RETURNS DOUBLE
RETURN NULL;
-- 3. Policy: created once, bound to the schema, masks every matching column except hr_admin and finance
CREATE OR REPLACE POLICY salary_policy
ON SCHEMA main.hr
COLUMN MASK main.security.mask_salary
TO `account users` EXCEPT hr_admin, finance
FOR TABLES
MATCH COLUMNS has_tag_value('sensitivity', 'high') AS salary
ON COLUMN salary;
Any column tagged sensitivity = high in main.hr, on any table, picks up the mask automatically. No new policy, no per-table wiring. Tagging the column is the only step.
ABAC security policies require DBR 16.4+ or serverless compute. Clusters on older runtimes will fail with a permission error when the policy is evaluated.
Tags
Tags are key-value metadata attached to securable objects: catalogs, schemas, tables, and columns. Unity Catalog has three types, and they serve different purposes.
| Type | Who sets it | Safe for ABAC triggers | Use for |
|---|---|---|---|
| Custom | Data owners, stewards | No — anyone with ALTER can change or remove it | Discovery, search, catalog navigation, team labeling |
| Governed | Authorized users (values policy-controlled) | Yes | Compliance classification, ABAC policy triggers |
| System | Databricks automatically | Yes | PII detection output, automatic classification results |
Custom tags
Free-form. Any user with ALTER privilege on an object can set them. Good for labeling by owner, team, or domain.
ALTER TABLE main.hr.employees
SET TAGS ('owner' = 'hr-team', 'domain' = 'hr', 'env' = 'prod');
Custom tags can technically appear in a FILTER TAG binding on a security policy, but you should not use them there. Because anyone with ALTER on the schema can change or remove the tag, the policy becomes bypassable:
-- Any data engineer with ALTER on the schema can do this, silently removing the masking policy
ALTER SCHEMA main.hr UNSET TAGS ('sensitivity');
Use custom tags for discovery and documentation only. For anything a security policy depends on, use a governed tag.
Governed tags
A governed tag has an admin-defined policy that controls which values are allowed and who can set them. This makes the tag a trusted, controlled signal. Only users the admin has authorized can apply or remove the value, so a security policy that filters on it cannot be quietly bypassed.
It also ensures consistency: every team uses sensitivity = 'pii', not pii, PII, or Personally Identifiable. These are the tags referenced in ABAC FILTER TAG bindings.
To create a tag policy: go to Data Explorer, select the catalog, open the Tags tab, and create a new tag policy. Define the key and the allowed values, then assign which groups can apply it.
-- Setting a governed tag value (only valid if 'pii' is an approved value for 'sensitivity',
-- and only possible if the caller is in a group authorized to apply this tag)
ALTER SCHEMA main.hr SET TAGS ('sensitivity' = 'pii');
System tags and automatic data classification
Unity Catalog can scan your tables and apply PII tags without any manual work. When enabled, Databricks samples column data, detects common PII types, and writes system-managed tags to each detected column.
To enable: open Data Explorer, select a catalog or schema, open the Data Classification tab, and turn on scanning.
After the scan, columns receive a databricks:columnPiiTypes tag with values like EMAIL_ADDRESS, PHONE_NUMBER, US_SOCIAL_SECURITY_NUMBER, CREDIT_CARD_NUMBER, and PERSON_NAME. You can write ABAC policies that filter on these system tags, so new tables are covered the moment they are scanned, with no manual tagging step.
This is the most scalable setup: classification runs on a schedule, finds new PII columns, and the policy covers them automatically.
System tags are read-only. You cannot set or remove a databricks: tag manually. They are updated when classification reruns.
Reference: Automatic data classification
Common PII patterns
Most organizations protect the same handful of column types. Two strategies exist for what non-privileged users see.
Redaction returns NULL or a fixed placeholder. No information survives. Use this when analysts have no reason to correlate on the value (salary, diagnosis, full SSN).
Hashing (pseudonymization) returns SHA2(value, 256). The hash is unreadable but deterministic: the same input always produces the same output. Data scientists can join user_events to customers on the hash token without ever seeing real addresses. Use this when cross-dataset joins matter and full anonymization is not required.
| Column | Who sees it | Others see | Common in |
|---|---|---|---|
salary DOUBLE | hr_admin, finance | NULL | All industries |
ssn STRING | hr_admin, compliance | 'XXX-XX-XXXX' or SHA-256 | HR, Insurance, Finance |
email STRING | data_owners, it_support | SHA-256 or '***@domain.com' | Retail, SaaS, Marketing |
card_number STRING | fraud_team | '****-****-****-1234' | Retail, Financial Services |
phone STRING | support_ops | '+X-XXX-XXX-1234' | Telecom, Retail, Healthcare |
dob DATE | medical_staff, actuaries | NULL | Healthcare, Insurance |
diagnosis STRING | clinical_staff | NULL | Healthcare (HIPAA) |
ip_address STRING | security_ops | 'XXX.XXX.X.X' | Tech, Security |
See the hands-on lab for the full UDF and policy implementation for each column type, including both redaction and SHA-256 variants.
Row filters
A column mask controls what a visible row shows. A row filter controls which rows are visible at all. Same mechanics, different target: instead of COLUMN MASK, a policy uses ROW FILTER with a function that returns BOOLEAN, and instead of ON COLUMN, it uses USING COLUMNS to pass in the tagged column the filter checks.
Use a row filter when different groups should see different slices of the same table: a regional manager sees their region, a support rep sees their queue, a business unit sees its own cost center. Use a column mask when the row is fine to see but a specific field on it isn't.
CREATE OR REPLACE FUNCTION main.security.filter_region(region STRING)
RETURNS BOOLEAN
RETURN is_account_group_member('region_emea') AND region = 'EMEA';
CREATE OR REPLACE POLICY region_policy
ON SCHEMA main.hr
ROW FILTER main.security.filter_region
TO `account users` EXCEPT hr_admin
FOR TABLES
MATCH COLUMNS has_tag_value('sensitivity', 'region') AS region
USING COLUMNS (region);
Everyone in region_emea sees only EMEA rows on any tagged table in main.hr. hr_admin is exempt and sees everything. See the hands-on lab for a full worked example with three regions.
In this section
- Hands-on lab: apply ABAC tag-driven policies to an employees table with salary, SSN, email, and credit card columns, including SHA-256 hashing and row-level filtering by region (~55 min)