Three objects, in a fixed chain: a governed tag says what a column is, a policy says who is affected and where, and a UDF does the actual work. Getting the chain straight is most of the route.
Unity Catalog ABAC — row filter and column mask policies — plus governed tags and automated data classification are generally available. They were Public Preview when most DACH permissions concepts on the shelf were written, which is why those documents contain a risk paragraph and a per-table fallback. Both can go.
The model is three objects in one direction of travel. The policy is not the logic; the logic is a UDF you can call, test and version like any other function.
Why
The alternative is per-table: ALTER TABLE … ALTER COLUMN … SET MASK on each table as it is built. Its failure mode is arithmetic — N tables times M sensitive columns, each needing one person to remember at the moment of creation. The table that gets missed is the one built in a hurry against a deadline, which is also the one most likely to be handed to someone outside the team.
Tag-driven inverts the default. A new table is protected the moment it is tagged, not the moment someone remembers to write a mask. The policy written today for gold.dim_customer covers the table someone builds next quarter, on its first query, unedited. The reviewer's job shrinks from "is there a mask on this table" to "are the columns tagged" — one query against information_schema.column_tags.
How
texttag → policy → UDF
GOVERNED TAG pii = email | phone | address account level, values fixed
|
| SET TAG ON COLUMN dwh_prod.gold.dim_customer.email pii = email
v
COLUMN TAG one column, one governed value
|
| MATCH COLUMNS has_tag('pii') AS pii_col
v
POLICY ON SCHEMA dwh_prod.gold TO ... EXCEPT ... evaluated per query
|
| COLUMN MASK dwh_prod.gold.mask_pii
v
UDF mask_pii(value STRING) RETURNS STRING
A policy attaches at catalog, schema or table and applies downward. It is evaluated dynamically against the tags as they are at query time — so moving a table into dwh_prod.gold puts it under the schema's policies with nobody redeploying anything.
Doing it
Delete the preview caveat and the per-table fallback from any permissions concept written before May 2026Leaving it in makes the whole document look unverified.
Fix the tag vocabulary before writing any policyThe policy is trivial once the vocabulary exists, and unfixable once tables are tagged three ways.
Attach policies at schema level (dwh_prod.gold) and write the reason downThe documented default is the highest applicable level, so a catalog-level reviewer will ask why not the catalog. Table-level policies are for genuine exceptions and should be countable on one hand.
Check compute before promising anythingABAC needs serverless, or DBR 16.4+ standard, or dedicated 16.4+ with fine-grained access control enabled.
Embedding it
Have the team tag one column and watch the mask appear on a table nobody editedThe propagation is the whole argument and it lands in thirty seconds.
Ask in every review of a new gold table: 'which of these columns carries a governed tag, and who checked?'Never 'did you add a mask'.
Give the tag vocabulary an owner on the client side, by nameA vocabulary owned by the consultant stops being maintained at handover.
When someone proposes a table-level policy, make them state why the schema-level one is wrongThe answer usually reveals a tagging gap, and the tagging gap is the real bug.
Defaults
Policies at schema level; table-level only as a written exception
One UDF per class of data, not per columnThe platform casts a mask's input and output, so one function covers every column whose type it is castable to, and a tag-driven policy resolves exactly one mask per column anyway.
Tags and policies deployed as DDL in the bundle, exercised in dwh_dev and dwh_test first
Gotchas
Quoting the preview status, which moved in May 2026A client who checks the docs while you are talking discounts everything else you said. Cite the date.
Assuming a policy protects a viewYou cannot attach an ABAC policy to a view; policies on the underlying tables evaluate with the session user's identity when the view is queried. A view built to hide columns and a policy are different mechanisms, and only one survives someone querying the base table.
Jobs on Databricks Runtime 15.4 LTS and below do not get masked dataThey fail outright on a protected table. The reflex fix is to add the job's service principal to EXCEPT, which does not downgrade its access; it removes protection entirely for everything that job writes.
Tag and policy changes take a few minutes to take effectEngineers tag a column, re-run the query, see raw values, conclude the policy is broken, and start editing a policy that was correct.
An ungoverned tag is a key-value pair anyone with APPLY TAG can invent. A governed tag is declared once at account level with a fixed list of allowed values, and applying one takes two permissions, not one: ASSIGN on the governed tag andAPPLY TAG on the object being tagged. ABAC policies read governed tags only, which is the point: a policy that matched free-text tags would be a policy defeated by a typo.
Why
Tag values are case-sensitive, and a policy matching has_tag_value('pii', 'email') does not match a column tagged PII = e_mail. Ungoverned, that divergence appears the first week two people tag columns independently, and it is invisible: the column looks tagged, the policy looks attached, and the data is readable. Governed values make the wrong tag impossible to apply rather than hard to find.
The vocabulary is also the durable deliverable. Policies get rewritten; a tag taxonomy that maps onto how the business talks about its data outlives the engagement and is what a new engineer reads to learn what is sensitive here.
How
sqlAccount level, once — the whole vocabulary for this warehouse
CREATE GOVERNED TAG pii
DESCRIPTION 'Kind of personal data this column carries'
VALUES ('email', 'phone', 'address');
CREATE GOVERNED TAG row_filter
DESCRIPTION 'Row-level access scheme this table is subject to'
VALUES ('territory');
CREATE GOVERNED TAG row_scope
DESCRIPTION 'Column a row-filter policy scopes on'
VALUES ('country');
Tags are then applied to catalogs, schemas, tables, views, volumes, functions and columns with SET TAG, in DDL, from the bundle — the same statements running against dwh_dev, dwh_test and dwh_prod.
sqlApplying them — the only three PII columns in the canonical model
SET TAG ON COLUMN dwh_prod.gold.dim_customer.email pii = email;
SET TAG ON COLUMN dwh_prod.gold.dim_customer.phone pii = phone;
SET TAG ON COLUMN dwh_prod.gold.dim_customer.street pii = address;
-- the row-filter pair: which table is filtered, and on which column
SET TAG ON TABLE dwh_prod.gold.dim_customer row_filter = territory;
SET TAG ON COLUMN dwh_prod.gold.dim_customer.country row_scope = country;
-- and the thing you will actually run in a review
SELECT table_name, column_name, tag_name, tag_value
FROM dwh_prod.information_schema.column_tags
WHERE schema_name = 'gold'
ORDER BY table_name, column_name;
Doing it
Declare the governed tags before the first table is taggedA value rename later means re-tagging every column that carries it.
Grant ASSIGN on each governed tag to the group that owns the domain, and APPLY TAG on the objects it ownsNeither permission works alone, and the pair is the real control over which policies apply — treat granting it like granting SELECT on gold.
Put every SET TAG statement in the bundle beside the DDL of the table it describesA new column and its tag then arrive in the same pull request.
Keep the vocabulary small enough to reciteThree PII values and two access tags cover this warehouse — one value per personal column in the model, and no value that nothing carries. A declared value with no column behind it reads as an oversight in the next review.
Embedding it
Run the vocabulary session with the data owners in the room, not the engineersThey know whether a delivery address is personal data here.
Have the team write the information_schema.column_tags review query themselves and put it in CIA vocabulary with no drift check is a wish.
Ask someone to tag a column in the Catalog Explorer UI, then ask how it reaches dwh_prodThe gap answers itself and nobody clicks tags again.
Ask who can un-tag a column in dwh_prodIf that list is longer than the list of people who can grant SELECT on gold, the team has not yet understood that the tag is the boundary — and the answer usually surprises them enough to fix itself.
Have them drop a referenced governed tag in dwh_dev and watch the queries failThey fail on tables the tag was never applied to. Nobody who has seen that error deletes a tag casually again.
Defaults
Governed tags for anything a policy reads; ungoverned tags are fine for cost and ownership metadata
SET TAG in DDL, in the bundle, never clicked in the UI — a clicked tag exists in one environment
Tag keys and values lower-case ASCII, matching the identifier rule in the architecture route
ASSIGN + APPLY TAG reviewed in the same pass as SELECT grants — same list, same owner, same quarterIt is the same power.
Deletion order fixed and written down: policy first, then the tag or function it referenced. Never the reverse
Gotchas
Tags do not inherit down to the column levelTagging gold.dim_customer with pii does nothing for a policy matching has_tag('pii') on columns — the columns are untagged and the data is served raw. This is the single most common 'the policy does not work' report, and the table tag makes it look like the job was done.
Tagging in Catalog Explorer, where it applies to one environmentThe policy is then tested successfully in dev and silently protects nothing in prod, because the columns there were never tagged.
Case sensitivity in both key and valuepii and PII are two different tags, and only one of them is the one your policy matches.
Renaming a governed tag value after tables are taggedThere is no cascade: the old value stays on the columns, the policy stops matching them, and nothing errors.
Tidying up an unused governed tagDropping a tag that any policy still references fails every query in that policy's scope with UC_ABAC_UNKNOWN_TAG_POLICY, whether or not the tag was ever applied to the table being queried — an estate-wide outage caused by deleting something that looked unused, from a UI that did not warn.
Assuming a column drop is always allowedUnity Catalog blocks dropping a column that carries a governed tag, so the schema migration that removes email fails mid-deploy on a table nobody expected to be special. Untag first, drop second.
Granting APPLY TAG broadly because tags feel like metadataWith governed tags they are not metadata, they are the switch that decides whether the mask applies — and un-tagging is invisible to every grant-based alert in the estate.