ABAC policies in Unity Catalog

kept in this browser

ABAC policies apply row filters and column masks from a metastore, catalog or schema, selecting objects by governed tag. A rule written once with CREATE POLICY covers every tagged table, present and future.

Checked against the Databricks documentation on · history

On the preview radar: Metastore-level ABAC policies

On this page14

What it is

ABAC (attribute-based access control) is how Unity Catalog applies access rules based on the attributes of objects rather than their names. The attribute is a governed tag (pii = email, sensitivity = high); the rule is a policy attached to a metastore (Beta), a catalog, a schema or a table that says: “for members of these groups, on columns with this tag, apply this mask” or “on every table with this tag, apply this row filter.”

Why it exists

Per-table row filters and column masks scale poorly: a hundred tables with an email column means a hundred ALTER TABLE statements, and the hundred-and-first table created tomorrow is born unprotected. With ABAC the rule is written once by the catalog owner, and every table that receives the tag inherits it automatically, with no way for the table owner to remove it.

How it works

Governed tags

Regular tags are free-form labels. Governed tags are defined at the account level (by an account admin or metastore admin) with a list of allowed values and permissions on who may assign them. In the UI they show a padlock. Only governed tags can be used in policy conditions. There are also predefined system tags (for example system.certification_status and the class.* family) that cannot be modified.

There are two condition functions:

  • has_tag('name'): the object or column has the tag, with any value;
  • has_tag_value('name', 'value'): the tag has exactly that value.

Anatomy of a policy

CREATE [ OR REPLACE ] POLICY policy_name
ON { METASTORE | CATALOG c | SCHEMA c.s | TABLE c.s.t }
[ COMMENT '...' ]
{ ROW FILTER function | COLUMN MASK function }
TO principal [, ...]
[ EXCEPT principal [, ...] ]
FOR TABLES
[ WHEN condition ]                                   -- on the object
[ MATCH COLUMNS condition AS alias [, ...] ]         -- on the columns
[ ON COLUMN alias ]                                  -- column mask only
[ USING COLUMNS ( alias | constant [, ...] ) ]
ClauseMeaning
ONwhere the policy is attached: it applies to everything below. ON METASTORE (Beta) takes no name — it means the metastore you are connected to, and naming one is a syntax error
TO / EXCEPTwho it applies to and who is exempt (often account users with EXCEPT for admins)
FOR TABLESthe target object type (tables, materialized views, streaming tables)
WHENcondition on the object, for example has_tag_value('sensitivity', 'high')
MATCH COLUMNS ... AS aliasselects columns by tag and gives them a name usable later
ON COLUMNwhich column to mask
USING COLUMNSadditional arguments to the function

The function is a SQL UDF, the same kind used by per-table filters; whoever queries the table needs EXECUTE on it. Creating or modifying a policy requires MANAGE on the ON object, or ownership — except ON METASTORE, which is the metastore admin’s alone.

Conditions are evaluated in the control plane, and three families of function can appear in them: the tag functions above, identity attribute functions (has_identity_attribute_value, has_identity_attribute_tag_match), which work only in the WHEN clause of a column mask and only from SQL, and context attribute functions (has_context_attribute, has_context_attribute_value), which read the request’s own context — what application is asking — and are the way to treat an agent acting on somebody’s behalf differently from that person at a keyboard.

Column mask

CREATE FUNCTION prod.sec.last_digits(value STRING, n INT)
RETURN IF(is_account_group_member('hr'), value, CONCAT('***', RIGHT(value, n)));

CREATE POLICY mask_tax_id
ON CATALOG prod
COMMENT 'Tax ID visible in full only to HR'
COLUMN MASK prod.sec.last_digits
TO `account users` EXCEPT `hr`
FOR TABLES
MATCH COLUMNS has_tag_value('pii', 'tax_id') AS tax
ON COLUMN tax
USING COLUMNS (4);

From this point on, every column in prod tagged pii = tax_id shows only its last four characters to anyone outside hr.

Row filter

CREATE FUNCTION prod.sec.domestic_only(region STRING)
RETURN region = 'IT';

CREATE POLICY filter_non_domestic
ON SCHEMA prod.sales
ROW FILTER prod.sec.domestic_only
TO `analysts`
FOR TABLES
WHEN has_tag_value('sensitivity', 'high')
MATCH COLUMNS has_tag('geo_region') AS region
USING COLUMNS (region);

From Python you run it with spark.sql(...), like any DDL.

Evaluation and conflicts

Policies are evaluated on every query: if the object carries the required tags, the policy applies. At runtime only one row filter and one mask per column can resolve for a given user: if two policies (or a policy and a per-table filter) apply the same function, the query proceeds; if they apply different functions, the query fails with an error. The comparison is on the functions, not on their results.

ABAC versus per-table filters

Per-table row filter / maskABAC policy
Scopeone tablecatalog, schema, or table and everything below it
Selectionby nameby governed tag
Who manages ittable ownercatalog/schema owner; the table owner cannot remove it
New tablesmust be configured by handcovered as soon as they are tagged
OpenSharing (Delta Sharing)noyes, if the share owner is exempt
When to usetable-specific logic, a few stable tablescross-cutting rules, a growing data estate

One policy for a whole metastore

A policy can now be attached to the metastore itself, which is the only scope that reaches catalogs that do not exist yet — including ones created after the policy was written. All four policy types support it, it takes the metastore admin role rather than MANAGE, and it is Beta.

Two things to know before reaching for it. Writing the SQL needs newer compute than using the result does (below), and Unity Catalog’s managed disaster recovery does not replicate metastore-level policies — objects arrive in the secondary metastore unprotected, which is the worst moment to discover it.

Requirements and limits

Three different runtime thresholds, which is a common source of confusion:

To do thisYou need
Query a table an ABAC policy protectsserverless, or DBR 16.4+ on standard compute, or DBR 16.4+ on dedicated compute with fine-grained access control filtering
Create or change a GRANT or DENY policy in SQLclassic compute on DBR 18 LTS or above
Create or change a metastore-level policy in SQLcompute on DBR 19 or above

Older runtimes cannot read protected tables at all: use EXCEPT to exempt the principals still on them.

The quotas, which the documentation now splits more finely than it used to:

LimitNumber
Policies per metastore, counting every object below it10,000
Policies attached directly to a metastore (Beta)100
Policies per catalog100
Policies per schema100
Policies per table50
Principals per policy, TO and EXCEPT together20
Column conditions in one MATCH COLUMNS, row filter policy6
Column conditions in one MATCH COLUMNS, column mask policy3

Row filters and column masks count separately from GRANT and DENY policies, which share a quota with each other.

Other limits worth carrying:

  • Materialized views and streaming tables: the refresh runs as the pipeline owner, so if the owner is subject to the policy the data is materialized already masked. Exempt them.
  • Time travel and clones — deep or shallow — fail on protected tables, and AI Search indexes do not enforce policies at all.
  • A metastore cannot carry a tag itself, so a metastore-level condition has to match something at catalog level or below.
  • GRANT and DENY cannot target external locations, storage credentials, shares, recipients, providers or connections.

GRANT and DENY policies

Two policy types beyond filters and masks, neither needed for the Associate exam.

A GRANT policy hands out privileges by tag instead of by name, and its targets are mostly the AI objects: models, model services, model provider services, MCP services and agent services, with EXECUTE, APPLY_TAG and READ_METADATA. What a user can actually do is the union of their direct grants and any policy that matches — and SHOW GRANTS shows only the first half, so SHOW EFFECTIVE POLICIES is the one that answers “why can they do that”.

A DENY policy (Beta) is the reason DENY is not a statement in Unity Catalog (see Privileges: GRANT, REVOKE, and DENY). It denies exactly one privilege, MANAGE_ACCESS_CONTROL, and it is absolute: it beats every grant including one held through a group or through owning the object, it inherits downward, and where two of them overlap the most restrictive wins. Metastore admins are always exempt, which is what stops somebody locking everyone out of their own metastore.

Example

A data steward tags prod.crm.customers.email with pii = email and prod.marketing.lead.email with the same tag. A single policy on prod covers both, plus every future table:

CREATE FUNCTION prod.sec.mask_email(email STRING)
RETURN CONCAT('***@', SPLIT_PART(email, '@', 2));

CREATE POLICY mask_email
ON CATALOG prod
COLUMN MASK prod.sec.mask_email
TO `account users` EXCEPT `privacy-office`
FOR TABLES
MATCH COLUMNS has_tag_value('pii', 'email') AS e
ON COLUMN e;

Common mistakes

  • Using a regular tag in the condition: only governed tags work.
  • Forgetting EXCEPT for the pipeline owner: the materialized views end up masked for everyone.
  • Attaching a policy and a SET MASK with different functions to the same column: the querying user gets an error, not double masking.
  • Assuming the table owner can remove the policy: only whoever created it at the higher level can.

Where this sits

Resources

6All resources

Related news

Report a problem with this page
What kind of problem?

Reports about "ABAC policies in Unity Catalog" go to the maintainer, not to a public thread.

Suggest a resource
What kind?

Nothing appears on the site automatically. A person reads every suggestion, checks the link and writes the note that goes with it.