A policy that was correct at go-live and has not been checked since is a claim, not a control. What you grant to groups rather than people, how you show the policies are still attached, and the review loop you build yourself before the platform gives you one.
Grants go to groups. The policy quota enforces it anyway — twenty principals per policy across TO and EXCEPT combined — but the reason is the one from the Unity Catalog route: an individual grant outlives the individual.
Why
A control is only a control if someone can demonstrate it is still in place. Query 2 earns its keep: it catches the column that arrived in a release and nobody tagged, which is how a tag-driven scheme decays — not policies being deleted, but data arriving outside the vocabulary.
Query 3 carries a trap worth knowing before an auditor finds it, and it has two halves that pull in opposite directions. In SQL there is no updatePolicy verb: an edit is CREATE OR REPLACE POLICY, which writes a second createPolicy row against the same name, so a naive count reads edits as creations. Group by policy name and treat repeats as edits.
The other half is the one that actually loses evidence. SQL is not the only path. Catalog Explorer has an explicit Edit-policy flow, and the SDK and REST API support genuine partial updates — update_policy with an update_mask, changing TO without restating EXCEPT. A client who edits either way produces no createPolicy row at all, so a review built on that verb over-reports nothing and under-reports exactly the changes made by the people least likely to be reading the DDL. If the team is allowed to edit in the UI, the inventory snapshot — not the audit verb — is what proves the policy set is unchanged.
The tag verbs in the same query close the other hole. Tagging is a security boundary: deleteEntityTagAssignment on dim_customer.email ends the masking of that column as surely as deletePolicy would, and it appears in none of the grant-based alerting this handbook otherwise recommends.
How
sqlThe complete grant set for an analyst group
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.
Reviewing it has one awkward edge: there is no information schema view for ABAC policies. The inventory is 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.
And SHOW alone does not produce the inventory a reviewer wants. It returns policy_name, policy_type, catalog, schema and comment — which policies reach the table and where they hang, but not the masking function and not the principals. Those come from DESCRIBE POLICY, one call per policy, returning info_name / info_value rows rather than columns. So "who is exempt from what" is two loops and a pivot, or one REST call — plan the collector accordingly instead of discovering it at the quarterly review. One nuance in your favour: viewing effective policies on a table needs no permission on the parent catalog or schema, so a table owner can see what applies to their table without being handed read access to every sibling policy in the estate.
sqlThe three review queries
-- 1. drive the inventory loop; run SHOW per row, concatenate client-side.
-- EXCLUDE, do not include. An allow-list of table types silently drops
-- EXTERNAL, FOREIGN and -- the ones that matter here -- MANAGED_SHALLOW_CLONE
-- and EXTERNAL_SHALLOW_CLONE, which are exactly the copies the exemption
-- discussion leaks into. Views are the only type that cannot carry a policy.
SELECT table_catalog, table_schema, table_name, table_type
FROM dwh_prod.information_schema.tables
WHERE table_schema = 'gold'
AND table_type <> 'VIEW';
-- 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 AND tag change in the last 30 days, with the actor.
-- The tag verbs are not optional extras: whoever can un-tag a column can
-- switch a mask off without touching a policy or a grant, so a review that
-- watches only createPolicy/deletePolicy has a hole the size of the scheme.
SELECT event_time,
user_identity.email AS actor,
action_name,
request_params AS params -- policy name, or the tagged object
FROM system.access.audit
WHERE service_name = 'unityCatalog'
AND action_name IN ('createPolicy', 'deletePolicy',
'createEntityTagAssignment', 'deleteEntityTagAssignment')
AND event_date >= current_date() - INTERVAL 30 DAYS
ORDER BY event_time DESC;
Doing it
Grant on gold onlyAnalysts reading silver is a contract violation before it is a governance one.
Run the three queries on a schedulePut the output somewhere a reviewer finds it without asking an engineer.
Capture the policy inventory the same way: SHOW per table, then DESCRIBE POLICY per policyAppend it to a Delta table. Two consecutive snapshots that differ is the only reliable detector of an edit made outside SQL.
Watch the quotas100 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 sensitiveIt records who read what, and it needs its own access rules.
Embedding it
Hand the access review to the client's own reviewer and sit silently through the first oneThe questions they cannot answer are your backlog.
Have the team schedule the untagged-column query as a jobRoute the failure to their rota, not to your inbox.
Ask them to explain to the auditor themselves why the SQL path has no updatePolicy verbThe same for why a UI edit leaves no createPolicy row at all. Owning the awkward detail is what makes the rest credible.
Retire your own access at handover, in front of them, and let them run the review that proves it
Defaults
Grants to groups only, on gold only, deployed as DDL
Access review quarterly: exemption table, untagged-column query and a captured inventory as exhibitsThe inventory is the only durable record, since the platform has no view to query later.
The captured inventory pairs SHOW EFFECTIVE POLICIES with DESCRIBE POLICY, or uses the REST APISHOW alone has no principals column, and principals are the half a reviewer asks about.
Alert on createPolicy, deletePolicy, createEntityTagAssignment and deleteEntityTagAssignmentAgainst dwh_prod, routed to a rota. A tag change moves protection exactly as a policy change does.
One editing path, agreed and written downIf policies may be edited in Catalog Explorer, the audit verb stops being evidence and the periodic inventory snapshot becomes the control.
Gotchas
Counting createPolicy events as creationsMost of them are edits, because in SQL CREATE OR REPLACE is the only way to change a policy and it emits the same verb.
Treating createPolicy as the complete record of policy changeCatalog Explorer's Edit flow and the SDK/REST update_policy partial update do not emit it, so a policy that was quietly re-scoped in the UI leaves your review query showing a clean month. Reconcile against a captured inventory, not against the verb.
Reviewing policies but not tagsSomeone with ASSIGN and APPLY TAG un-tags dim_customer.email, the mask stops applying, the grants are untouched, the policy is untouched — and every alert in this handbook stays silent unless the tag-assignment actions are in the query.
Building the review inventory from SHOW EFFECTIVE POLICIES alone, then being asked who is exemptThe result carries no function and no principals; you need DESCRIBE POLICY per policy or the REST API, and finding that out in front of the auditor is the expensive way.
Filtering the inventory loop to an allow-list of table typesMANAGED, MATERIALIZED_VIEW, STREAMING_TABLE silently omits EXTERNAL, FOREIGN and both shallow-clone types — so the clone that a policy does not cover is also the one your review never looked at.
Expecting system.information_schema.effective_policiesIt 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 policyIt 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 goldIt 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 themselvesSHOW EFFECTIVE POLICIES and DESCRIBE POLICY require 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
What
Governance Hub is an account-level console with three pages — Data, AI and Cost — reporting posture with prioritised insights and recommendations. The Data page is the one this route cares about: object access, asset usage, governed tags, data classification and data quality monitoring, across the estate. It introduces no new permissions; every view respects the Unity Catalog grants the viewer already has.
It is in Beta, and — unlike most preview features — an account admin switches it on themselves from the account console Previews page. Access is not gated by Databricks and there is nothing to request. So "we cannot get access yet" is not a reason to defer; the honest reasons to be careful are that it is Beta and that the data takes up to a day to appear after enabling.
The reflex it produces in most teams is still the wrong one: enable it, look at it, and call that governance. The Hub is a view over answers you can already compute. Build the loop against 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.
Why
The failure this prevents is treating governance as configuration — something you set up in month two and then consider done. It is not a state, it is a drift problem. New gold tables appear untagged. Exemptions granted for one migration are never withdrawn. An owner leaves and their objects keep their owner. None of that raises an error, and every one of it is invisible until an audit or a leak.
So the answer to "what do we do with it beyond looking at dashboards" is that a posture number is worthless on its own and useful in three specific mechanisms: as a trend, as a queue, and as a gate. A dashboard nobody has attached to a decision is a screenshot in a steering pack.
Three ways a posture number changes behaviour
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.
How
sqlsrc/dwh/governance/posture.sql — the four numbers a review actually moves
-- 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';
Doing it
Schedule the posture query weekly and append to a Delta tableA snapshot you can trend beats a console you have to open.
Make the release gate a CI check against the same SQLA new domain then cannot enter dwh_prod with untagged personal columns.
Turn every non-zero metric into a ticket with an owner and a dateOn the day it appears, not at the quarterly review.
When Governance Hub access arrives, reconcile its numbers against yours before trusting eitherA disagreement is a finding about your query or their scope, and you want to know which.
Embedding it
Give the posture query to the team as their artefact and let them add the fifth metricThe one they add tells you what they are actually worried about.
Chair the first governance review, then hand the chair over and attend the second as a guestGovernance that needs the consultant in the room has not transferred.
When a metric goes the wrong way, resist fixing it — assign itThe point of the queue is that it has owners who are not you.
Ask them what they would do differently if Governance Hub arrived tomorrowIf the honest answer is 'nothing, we would just read it there', the loop is genuinely theirs.
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 operationsNothing 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 itBeta 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 deliverableA 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 vocabularyScoped that way it is 100% forever and silently misses the day a new gold table grows an email column, which is the only event it existed to catch.
table_owner LIKE '%@%' is a heuristic, not a ruleIt assumes groups are not named with an email address; check the client's naming before you trust the count, because a group called data-eng@corp.example makes this metric read zero forever.