Guides

Unity Catalog grants that actually work

The symptom: you granted SELECT on a table and the analyst still gets permission denied; or the opposite, a contractor can read everything because someone granted at the catalog level in a hurry. The cause is that Unity Catalog permissions are three levels deep and most grants are made at the wrong one.

The model in one paragraph

Objects live in a hierarchy: metastore, catalog, schema, table. To read a table, a principal needs USE CATALOG on the catalog, USE SCHEMA on the schema, and SELECT on the table. Granting SELECT alone does nothing visible, because the principal cannot traverse to the table. That single fact explains most "but I granted it" tickets. Privileges granted on a parent are inherited by its children, which is convenient and also how the contractor ended up reading everything.

The fix: grant to groups, at the lowest useful level

1. Create groups at the account level, not in the workspace

Account-level groups (analysts, data_eng, finance_readers) are the unit of access. Never grant to an individual user; when they leave, the grant stays behind. Sync the groups from your identity provider so membership is managed where HR already manages it.

2. Grant traversal plus the one thing the group needs

-- Analysts: read gold, nothing else
GRANT USE CATALOG ON CATALOG taxi TO `analysts`;
GRANT USE SCHEMA  ON SCHEMA  taxi.gold TO `analysts`;
GRANT SELECT      ON SCHEMA  taxi.gold TO `analysts`;   -- inherited by every table in gold, present and future

-- Engineers: read silver and the quarantine table too
GRANT USE SCHEMA ON SCHEMA taxi.silver TO `data_eng`;
GRANT SELECT     ON SCHEMA taxi.silver TO `data_eng`;

SELECT ON SCHEMA is the right grain for a reporting layer: new gold tables are readable the day they appear, and nothing outside gold is. Granting SELECT ON CATALOG would also expose bronze and silver, which is the contractor problem.

3. Writes belong to service principals

No human group gets MODIFY or CREATE TABLE on production schemas. Pipelines run as a service principal that owns those privileges, and the audit log then shows which job changed a table, not which person happened to be logged in. People who need to experiment get their own sandbox schema.

GRANT USE SCHEMA, CREATE TABLE, MODIFY ON SCHEMA taxi.silver TO `sp-pipelines`;
GRANT USE SCHEMA, CREATE TABLE, MODIFY ON SCHEMA taxi.gold   TO `sp-pipelines`;

4. Filter rows and mask columns in the catalog, not in the report

If the finance group may only see its region, put that rule on the table so every tool inherits it. A row filter is a SQL function attached to the table; a column mask is the same idea for a single column.

CREATE FUNCTION taxi.gold.region_filter(region STRING)
RETURN is_account_group_member('all_regions') OR region = current_user_region();

ALTER TABLE taxi.gold.revenue_by_region SET ROW FILTER taxi.gold.region_filter ON (region);

(current_user_region() stands for whatever lookup you use, usually a small mapping table from user to region.) Power BI, a notebook and a SQL warehouse now all return the same filtered rows for the same person, and the rule lives in exactly one place.

Diagnosing permission denied

Work down the hierarchy. The error message names the privilege and the object; check that one, then its parent.

SHOW GRANTS ON CATALOG taxi;
SHOW GRANTS ON SCHEMA  taxi.gold;
SHOW GRANTS ON TABLE   taxi.gold.trips_by_zone_hour;
SHOW GRANTS `analysts`;        -- everything this group holds, anywhere

The three common causes, in order: missing USE SCHEMA (the grant was made on the table only); the user is not actually in the group they think they are in (check account groups, not workspace groups); the object is owned by a user who has since left and the owner's privileges were never transferred (ALTER TABLE ... OWNER TO fixes it).

Proving who read what

The audit log is a system table, so "who accessed the revenue table last month" is a query rather than a support ticket.

SELECT event_time, user_identity.email, action_name, request_params.full_name_arg AS object
FROM system.access.audit
WHERE request_params.full_name_arg LIKE 'taxi.gold.%'
  AND event_date >= current_date() - INTERVAL 30 DAYS
ORDER BY event_time DESC;

Pair it with system.access.table_lineage to see which reports depend on a table before you change it.

Two habits that keep it clean

  • A grants file in the repository. All grants live in one SQL script that is applied by a job, so the current state is readable and reviewable, and a grant made by hand in the UI stands out as the exception.
  • A quarterly read of SHOW GRANTS per group. Takes ten minutes, finds the catalog-level grant someone made during an incident.

When to ask for help

If the workspace predates Unity Catalog and tables still live in the legacy Hive metastore, the migration is the real job and the grants come with it. If the catalog exists but nobody can say who can read what, a one-day audit produces the grants file and the list of surprises.

Related: who can read what in the lakehouse FAQ, Unity Catalog and service principal in the glossary.

Cannot tell who can read the revenue table?

Send the output of SHOW GRANTS on the schema, names removed. We reply within a working day with what is wrong and the script that fixes it.