16Access & ABAC
This route decides how a column stops being readable: by tag, once, at schema level — not by a mask written per table by whoever remembered. ABAC went generally available in May 2026, which retires the preview-risk paragraph and the per-table fallback that every permissions concept written before then still carries. The work in week one is the tag vocabulary. The policies are ten lines each and follow from it.
The tag-driven model
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.
#ABAC is GA, and that changes the design
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 STRINGdwh_prod.gold puts it under the schema's policies with nobody redeploying anything.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.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.Doing it
- Delete the preview caveat and the per-table fallback from any permissions concept written before May 2026 — leaving it in makes the whole document look unverified.
- Fix the tag vocabulary before writing any policy; the policy is trivial once the vocabulary exists, and unfixable once tables are tagged three ways.
- Attach policies at schema level (dwh_prod.gold). Table-level policies are for genuine exceptions and should be countable on one hand.
- Check compute before promising anything: ABAC needs serverless, or DBR 16.4+ standard, or dedicated 16.4+ with fine-grained access control enabled.
Defaults
- Policies at schema level; table-level only as a written exception.
- One UDF per class of data, not per column — only one distinct mask resolves per table and user anyway.
- Tags and policies deployed as DDL in the bundle, exercised in dwh_dev and dwh_test first.
Gotchas
- Quoting the preview status. It moved in May 2026, and a client who checks the docs while you are talking discounts everything else you said. Cite the date.
- Assuming a policy protects a view. You 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 data — they 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 effect. Engineers tag a column, re-run the query, see raw values, conclude the policy is broken, and start editing a policy that was correct.
- changedUnity Catalog ABAC — row filter and column mask policies — plus governed tags and automated data classification are GENERALLY AVAILABLE.Stand 29.07.2026
- check firstABAC GRANT policies — dynamic privilege grants — are Beta and currently scoped to EXECUTE on models.Stand 29.07.2026
- check firstGovernance Hub is in BETA. It is an account-level console with three pages — Data, AI and Cost. The Data page covers object access, asset usage, governed tags, data classification and data quality monitoring. An ACCOUNT ADMIN ENABLES IT THEMSELVES from the account console Previews page; access is not gated by Databricks.Stand 29.07.2026
#Governed tags are the vocabulary
APPLY TAG can invent. A governed tag is declared once at account level with a fixed list of allowed values, and needs the ASSIGN permission to apply. 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.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');SET TAG, in DDL, from the bundle — the same statements running against dwh_dev, dwh_test and dwh_prod.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;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.Doing it
- Declare the governed tags before the first table is tagged. A value rename later means re-tagging every column that carries it.
- Grant ASSIGN on each governed tag to the group that owns the domain, not to everyone with APPLY TAG.
- Put every SET TAG statement in the bundle beside the DDL of the table it describes, so a new column and its tag arrive in the same pull request.
- Keep the vocabulary small enough to recite. Three 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.
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.
Gotchas
- Tags do not inherit down to the column level. Tagging
gold.dim_customerwithpiidoes nothing for a policy matchinghas_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. It applies to one environment. The 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 value. pii 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 tagged. There is no cascade: the old value stays on the columns, the policy stops matching them, and nothing errors.
Policies on dim_customer
Two policies cover this warehouse: one masks the personal columns for everyone who is not on a short list, one restricts rows to a sales territory. Both are attached to the gold schema and neither names a table.
#One column mask for the whole PII class
gold.dim_customer are STRING, so one function covers them.CREATE OR REPLACE FUNCTION dwh_prod.gold.mask_pii(value STRING)
RETURNS STRING
COMMENT 'Redact personal data. NULL stays NULL so counts do not move.'
RETURN CASE WHEN value IS NULL THEN NULL ELSE '***' END;
CREATE OR REPLACE POLICY mask_customer_pii
ON SCHEMA dwh_prod.gold
COMMENT 'Redact every column tagged pii for all but the named readers'
COLUMN MASK dwh_prod.gold.mask_pii
TO `account users`
EXCEPT `dwh_pii_readers`, `dwh_prod_pipelines`
FOR TABLES
MATCH COLUMNS has_tag('pii') AS pii_col
ON COLUMN pii_col;MATCH COLUMNS binds the alias pii_col to every column satisfying the condition, and ON COLUMN pii_col applies the mask to each of them. Three columns, one policy, and the next tagged column joins without an edit.SELECT customer_id, customer_name, email, city, country
FROM dwh_prod.gold.dim_customer
WHERE _tech_is_current;
-- customer_id | customer_name | email | city | country
-- C-100482 | Nordbau Handel | *** | Hannover | DE
-- C-100483 | Alpin Technik | *** | Innsbruck | AT
SHOW EFFECTIVE POLICIES ON TABLE dwh_prod.gold.dim_customer;
-- policy_name | policy_type | function | principalsCOLUMN_MASKS_FEATURE_NOT_SUPPORTED.MULTIPLE_MASKS. (Row filters are the stricter case: one distinct filter per table and user.)EXCEPT, both named in the permissions concept. In the per-table world, the answer to 'who can read customer email' is a sweep of every mask on every table.Doing it
- Write mask_pii once, in gold, and grant EXECUTE on it to the same principals the policy names.
- Keep NULL returning NULL. A mask that turns NULL into '***' changes COUNT(email) and someone will reconcile against it.
- Deploy the policy to dwh_dev and dwh_test with the same statement — a policy that exists only in prod is a policy first exercised in prod.
- Verify with SHOW EFFECTIVE POLICIES ON TABLE against a table you did not name in the policy. That is the proof the schema attachment works.
Defaults
- One mask UDF per data class; differentiate with tags, not with functions.
- Return type matches column type exactly — a STRING mask on a DECIMAL column is rejected, not coerced.
- EXCEPT lists contain groups only, and every group is justified in writing.
Gotchas
- A second column-mask policy on the same table for the same principal. Only one distinct mask can resolve, so the query errors instead of masking — and it errors for the analyst, at 08:00, on a dashboard, not for the person who added the policy.
- Masking as a substitute for deletion. The value is still stored, still in the Delta history, and still leaves the platform in any export by an exempted identity. Under GDPR the column remains personal data — see the classification route.
TO `account users`includes service principals. Every job that is not explicitly exempted starts reading '***', usually discovered when a downstream table quietly fills with asterisks that then survive into a report.- Forgetting EXECUTE on the UDF. Creating a policy needs MANAGE on the securable and EXECUTE on the masking function, so the statement is rejected outright — and the error names the function rather than the policy, which reads as if the function were never deployed. The bundle order is function, then grant, then policy; get it wrong and the deploy fails on a green-looking pull request.
CREATE OR REPLACE POLICYreplaces the whole definition, not the clauses you retyped. There is no partial edit: ship a change toTOwithout re-statingEXCEPTand the exemptions are gone, the pipelines start writing'***'on the next run, and the pull request looks like a two-line change. Keep the policy DDL in one file and always edit it whole.
#Territory row filter
CREATE OR REPLACE FUNCTION dwh_prod.gold.in_sales_territory(country STRING)
RETURNS BOOLEAN
COMMENT 'True if the caller belongs to the territory group for this country'
RETURN is_account_group_member(concat('dwh_territory_', lower(country)));
CREATE OR REPLACE POLICY territory_rows
ON SCHEMA dwh_prod.gold
COMMENT 'Sales roles see only the countries their territory group covers'
ROW FILTER dwh_prod.gold.in_sales_territory
TO `dwh_sales`
EXCEPT `dwh_prod_pipelines`
FOR TABLES
WHEN has_tag_value('row_filter', 'territory')
MATCH COLUMNS has_tag_value('row_scope', 'country') AS country_col
USING COLUMNS (country_col);WHEN selects which tables the policy touches, from their tags. MATCH COLUMNS finds the scoping column, and USING COLUMNS passes it as the function's argument. Tag a second table row_filter = territory with a column tagged row_scope = country and it is filtered the same way, immediately.EXCEPT dwh_prod_pipelines line looks redundant beside TO dwh_sales — a pipeline is not a salesperson — and it is not. Service principals get added to business groups for an unrelated access request and nobody rereads the policy; a territory-scoped pipeline then writes a partial table, and no test tells a partial load apart from a quiet day.dim_customer_de, dim_customer_at — and it multiplies: four countries times every table anyone wants scoped, each view with its own grants, each drifting from the base table's schema the first time a column is added.dwh_territory_ch is the entire onboarding step. No view, no grant, no deployment.Doing it
- Derive the group names from the data: country values are DE, AT, CH and NL, so the groups are dwh_territory_de, _at, _ch, _nl and the mapping is a lower() call rather than a lookup table.
- Keep the filter function to group membership and cheap expressions — it runs per row on every query against every covered table.
- Tag the table and the scoping column together, in one statement pair, so a table can never be marked filtered without a column to filter on.
- Test as a member of exactly one territory group, and as a member of none.
Defaults
- One filter function, parameterised by the scoping column; territories are groups, not code.
- Table tag plus column tag applied together — the pair is the contract.
- Pipeline identities exempted, so builds are not silently territory-scoped.
- The asymmetry between filtered dimensions and unfiltered facts written into the permissions concept.
Gotchas
- A user in no territory group sees zero rows, with no error. Their dashboard shows €0 revenue and they report a data outage; you spend the morning in the pipeline logs. Give every account exactly one territory group at creation and assert it in the access review.
- The function runs per row. A filter that joins to a mapping table instead of calling is_account_group_member turns every scan of every covered table into a join, and the cost surfaces as a warehouse that got slower for no attributable reason.
- Only one distinct row filter resolves per table and user, exactly as with masks. A second policy with a different function on the same table fails the query rather than intersecting the conditions.
- Tagging a table
row_filter = territorywhen no column carriesrow_scope. Nothing errors and nothing is filtered — the table looks governed in the inventory while serving every row. - The unknown member disappears.
dim_customercarries an UNKNOWN row so unresolvable facts stay visible instead of being dropped; its country resolves to adwh_territory_group that does not exist, and a NULL country makesconcatitself NULL, so the row is filtered out for every sales user. The one row that exists to make bad data visible is the one row the filter hides. Decide who owns the unknown territory and coalesce it inside the function.
Limits and review
Two things separate a permissions concept that survives an audit from one that reads well: knowing precisely what an exemption grants, and being able to show the policies are still attached.
#What EXCEPT buys, and what it costs
EXCEPT is not a lower privilege level. It is a full bypass of the policy for that principal, on every covered table, for every covered column. There is no partial exemption.| Operation on gold.dim_customer | Subject to the policy | Named in EXCEPT |
|---|---|---|
| SELECT email | '***' | the address |
| VERSION AS OF 42 / TIMESTAMP AS OF | query fails — policies cannot be evaluated against a historical snapshot | the address, at that version |
| CREATE TABLE … DEEP CLONE / SHALLOW CLONE | unsupported, fails | succeeds — and the clone is a new securable |
| Delta Sharing as share owner | share creation blocked | shares the unmasked column |
| Building a materialized view over the table | pipeline fails | materialises unmasked values |
dwh_prod.gold.dim_customer into a schema the policy does not cover and the copy is unprotected, whatever the source was.Doing it
- Enumerate every principal in every EXCEPT clause into one table in the permissions concept, with the operation that forced the exemption next to it.
- Tag the outputs of exempted pipelines — the materialized view, the aggregate, the export table — not just the sources.
- Decide where clones may land, and make the target schema covered by the same policies as the source.
- Check whether anything depends on time travel — a restore runbook, a reconciliation query — before the policy lands, because it will stop working.
Defaults
- One documented exemption table, reviewed quarterly, with an owner per row.
- Exempt service principals, never people, for machine operations.
- Outputs of exempted pipelines carry the same governed tags as their inputs.
- Clone and export targets live inside the policy's scope, or the export does not happen.
Gotchas
- Adding a service principal to EXCEPT to fix a failing job. It fixes the job and removes protection from everything that job touches, and it looks like a one-word edit in a pull request.
- Time travel silently leaving the toolbox. Nobody notices until an incident, when the query that would have shown yesterday's state fails on a table that is now protected.
- A materialized view or aggregate built from tagged sources but never tagged itself. The mask holds on dim_customer and the same email sits unmasked one table downstream — which is where BI reads.
- Treating masking as anonymisation. It is neither; see the classification route, where the difference decides whether the data is in scope at all.
#Group grants, and proving they are still right
TO and EXCEPT combined — but the reason is the one from the Unity Catalog route: an individual grant outlives the individual.GRANT USE CATALOG ON CATALOG dwh_prod TO `dwh_analysts`;
GRANT USE SCHEMA ON SCHEMA dwh_prod.gold TO `dwh_analysts`;
GRANT SELECT ON SCHEMA dwh_prod.gold TO `dwh_analysts`;
GRANT EXECUTE ON FUNCTION dwh_prod.gold.mask_pii TO `dwh_analysts`;
-- gold only. No grant on silver, no grant on bronze.
-- Territory scoping is membership of dwh_territory_*, not a grant.SHOW EFFECTIVE POLICIES ON TABLE looped over the table list, or the Unity Catalog REST API. The headline count is something you assemble, not something you SELECT.-- 1. drive the inventory loop; run SHOW per row, concatenate client-side.
-- Materialized views and streaming tables carry policies too, so filtering to
-- MANAGED alone drops exactly the downstream copies the exemptions leak into.
SELECT table_catalog, table_schema, table_name, table_type
FROM dwh_prod.information_schema.tables
WHERE table_schema = 'gold'
AND table_type IN ('MANAGED', 'MATERIALIZED_VIEW', 'STREAMING_TABLE');
-- 2. candidate personal columns carrying no pii tag. Zero rows is a pass.
-- The list is the vocabulary, not dim_customer's column list: it fires when
-- any gold table grows an email or a street. customer_name is deliberately
-- absent -- in this B2B model it is the company name, not personal data.
SELECT c.table_name, c.column_name
FROM dwh_prod.information_schema.columns c
LEFT JOIN dwh_prod.information_schema.column_tags t
ON t.schema_name = c.table_schema
AND t.table_name = c.table_name
AND t.column_name = c.column_name
AND t.tag_name = 'pii'
WHERE c.table_schema = 'gold'
AND c.column_name IN ('email', 'phone', 'street')
AND t.tag_name IS NULL;
-- 3. every policy change in the last 30 days, with the editor
SELECT event_time,
user_identity.email AS actor,
action_name,
request_params['name'] AS policy_name
FROM system.access.audit
WHERE service_name = 'unityCatalog'
AND action_name IN ('createPolicy', 'deletePolicy')
AND event_date >= current_date() - INTERVAL 30 DAYS
ORDER BY event_time DESC;updatePolicy verb. An edit is CREATE OR REPLACE POLICY, which writes a second createPolicy row against the same name — so a naive change count under-reports edits by exactly the rate at which the team edits policies, that being the only edit mechanism on offer. Group by policy name and treat repeats as edits.Doing it
- Grant on gold only. Analysts reading silver is a contract violation before it is a governance one.
- Run the three queries on a schedule and put the output somewhere a reviewer finds it without asking an engineer.
- Watch the quotas: 100 policies per catalog or schema, 50 per table, 20 principals per policy across TO and EXCEPT, and 3 column conditions per MATCH COLUMNS clause. They refuse the next policy rather than degrading, usually mid-rollout.
- Treat the audit table itself as sensitive — it records who read what, and it needs its own access rules.
Defaults
- Grants to groups only, on gold only, deployed as DDL.
- Access review quarterly, with the exemption table, the untagged-column query and a captured policy inventory as its exhibits — the inventory is the only durable record, since the platform has no view to query later.
- Alert on createPolicy and deletePolicy against dwh_prod, routed to a rota.
Gotchas
- Counting
createPolicyevents as creations. Most of them are edits, because CREATE OR REPLACE is the only way to change a policy and it emits the same verb. - Expecting
system.information_schema.effective_policies. It does not exist. Anyone who writes a review query against it gets a table-not-found and concludes ABAC is not enabled. - Hitting the 20-principal ceiling on a policy. It appears the day someone lists individual service principals in EXCEPT instead of one group, and the policy edit is rejected while the old version stays live.
- Granting SELECT on the catalog rather than on gold. It reaches bronze, where the raw SAP extract sits with no mask, no tag and no policy — the whole scheme bypassed by one convenient wildcard.
- Assuming the reviewer can assemble the inventory themselves.
SHOW EFFECTIVE POLICIESandDESCRIBE POLICYrequire MANAGE on the securable or ownership of it — which a data-protection officer does not have and should not be given. Somebody with MANAGE captures the inventory on a schedule, or the quarterly review stalls waiting on an engineer's calendar.
#Governance Hub, and the loop you build before you get it
information_schema and system.* regardless — then the Hub is the reporting layer on a process that already exists, rather than a console nobody has attached to a decision.-- One row per metric, so the output is a posture SNAPSHOT you can trend.
-- These are the same questions the Hub's Data page consolidates; computing
-- them yourself is what makes the Hub an upgrade rather than a migration.
-- 1. classification coverage: candidate personal columns carrying a pii tag.
SELECT 'pii_coverage_pct' AS metric,
ROUND(100.0 * count_if(t.tag_name IS NOT NULL) / nullif(count(*), 0), 1) AS value
FROM dwh_prod.information_schema.columns c
LEFT JOIN dwh_prod.information_schema.column_tags t
ON t.schema_name = c.table_schema
AND t.table_name = c.table_name
AND t.column_name = c.column_name
AND t.tag_name = 'pii'
WHERE c.table_schema = 'gold'
AND c.column_name IN ('email', 'phone', 'street')
UNION ALL
-- 2. ownership decay: production objects owned by a human, not a group.
SELECT 'tables_owned_by_individual',
count(*)
FROM dwh_prod.information_schema.tables
WHERE table_schema IN ('silver', 'gold')
AND table_owner LIKE '%@%' -- a group name has no @
UNION ALL
-- 3. discoverability: gold tables shipped with no COMMENT.
SELECT 'gold_tables_without_comment',
count_if(comment IS NULL OR trim(comment) = '')
FROM dwh_prod.information_schema.tables
WHERE table_schema = 'gold'
UNION ALL
-- 4. staleness: gold tables not written in 48h. Freshness is governance too --
-- an abandoned table is a table nobody can vouch for.
SELECT 'gold_tables_stale_48h',
count_if(last_altered < current_timestamp() - INTERVAL 48 HOURS)
FROM dwh_prod.information_schema.tables
WHERE table_schema = 'gold';| Mechanism | How it works | What it prevents |
|---|---|---|
| Trend | Snapshot the four metrics weekly into a Delta table. The review reads the delta, not the level. | The absolute number is always arguable and always argued. A number that got worse this week is not. |
| Queue | Every non-zero count becomes a ticket with a named owner and a date — not a slide. | Findings that are reported repeatedly and fixed never. If nothing is assigned, nothing changes. |
| Gate | Coverage must be 100% and individual-owned tables 0 before a new domain is allowed into dwh_prod. | Governance debt entering production faster than it is repaid, which is the only way it ever accumulates. |
Doing it
- Schedule the posture query weekly and append to a Delta table — a snapshot you can trend beats a console you have to open.
- Make the release gate a CI check against the same SQL, so a new domain cannot enter dwh_prod with untagged personal columns.
- Turn every non-zero metric into a ticket with an owner and a date on the day it appears, not at the quarterly review.
- When Governance Hub access arrives, reconcile its numbers against yours before trusting either — a disagreement is a finding about your query or their scope, and you want to know which.
Defaults
- Compute posture yourself today; treat any console as a nicer view over numbers you already own.
- Trend the metrics, do not just level-check them — the delta is the reviewable thing.
- Every non-zero metric has a named owner and a date, or it is not being governed.
- Coverage is a release gate on new domains, enforced in CI, not a quarterly conversation.
- Nothing in a signed permissions concept depends on a private-preview feature.
Gotchas
- Waiting for Governance Hub before starting governance operations. Nothing is being waited for — an account admin can enable it from the Previews page today — and a team that arrives at GA with a console and no process to point it at has gained a screenshot, not governance.
- Repeating "it is private preview, we cannot get it". That was the announcement wording; the documentation says Beta with account-admin self-enable. The cost of the stale version is a quarter of deferred governance work justified by a blocker that does not exist.
- Designing a signed client deliverable around it. Beta means the shape can still change, so a permissions concept that references it acquires a dependency you cannot date — enable it, use it, but govern with the GA controls.
- Treating the posture number as the deliverable. A dashboard with no owner and no gate is reported quarterly, admired, and never acted on — the metric stays flat for a year and everyone stops reading it.
- Measuring coverage against
dim_customer's column list instead of the tag vocabulary. Scoped that way it is 100% forever and silently misses the day a new gold table grows anemailcolumn, which is the only event it existed to catch. table_owner LIKE '%@%'is a heuristic, not a rule. It assumes groups are not named with an email address; check the client's naming before you trust the count, because a group calleddata-eng@corp.examplemakes this metric read zero forever.
- check firstGovernance Hub is in BETA. It is an account-level console with three pages — Data, AI and Cost. The Data page covers object access, asset usage, governed tags, data classification and data quality monitoring. An ACCOUNT ADMIN ENABLES IT THEMSELVES from the account console Previews page; access is not gated by Databricks.Stand 29.07.2026
- changedUnity Catalog ABAC — row filter and column mask policies — plus governed tags and automated data classification are GENERALLY AVAILABLE.Stand 29.07.2026
Straight to the documentation
The generic platform material lives in the official docs and is not restated here. These are the pages worth having open while you work through this route.
- Attribute-based access control in Unity Catalog — the model itself — attributes, where policies attach, and what dynamic evaluation means
- ABAC policies — CREATE POLICY reference — the exact grammar, including the FOR TABLES clause and the ON COLUMN / USING COLUMNS pair people get backwards
- ABAC requirements, quotas and limitations — read before promising anything: the DBR 16.4 floor, the quota table, and the clone / time-travel / view limits
- Create and manage governed tags — CREATE GOVERNED TAG, allowed values, and the ASSIGN permission that decides who may apply them
- SET TAG / UNSET TAG syntax — every securable form, including SET TAG ON COLUMN — the one that actually drives a mask