Multi-Domain Data Governance
with ABAC

Real-world enterprises have data owned by multiple business domains — HR, Finance, Marketing. This tutorial shows how to combine 2D tagging (domain × sensitivity) with row filtering and column masking so each team sees only their domain's data, at the right sensitivity level. Includes a Telco CPNI compliance example under FCC 47 U.S.C. § 222.

The Problem

A single table holds data from multiple business domains. Each domain owns certain columns, and each column has a sensitivity level. The challenge: apply the right mask per domain × sensitivity combination, without policies conflicting with each other.

👥

HR Domain

  • employee_name internal
  • ssn confidential
💰

Finance Domain

  • cost_center internal
  • salary_band confidential
📣

Marketing Domain

  • email internal
  • customer_list confidential

The 2D Tagging Pattern

Every sensitive column gets two tags: one for which domain owns it, one for how sensitive it is. Policies then match on AND conditions — both tags must match. This ensures exactly one policy applies per column per user, eliminating conflicts.

SQL
-- Two tags per column: domain ownership + sensitivity level
ALTER TABLE {catalog}.{schema}.employee_records
  ALTER COLUMN employee_name SET TAGS ('abac_tut_domain' = 'hr', 'abac_tut_sensitivity' = 'internal')

ALTER TABLE {catalog}.{schema}.employee_records
  ALTER COLUMN ssn SET TAGS ('abac_tut_domain' = 'hr', 'abac_tut_sensitivity' = 'confidential')

Policy Matrix

Six policies — one per domain × sensitivity combination. Each uses an AND condition and exempts only the owning domain group.

Domain internal → partial_mask confidential → redact
HR
EXCEPT abac_tut_hr_team
mask_internal_hr mask_confidential_hr
Finance
EXCEPT abac_tut_finance_team
mask_internal_finance mask_confidential_finance
Marketing
EXCEPT abac_tut_marketing_team
mask_internal_marketing mask_confidential_marketing
SQL
-- AND condition ensures exactly one policy matches per column
CREATE OR REPLACE POLICY mask_internal_hr
ON SCHEMA {catalog}.{schema}
COLUMN MASK {catalog}.{schema}.partial_mask
TO `account users` EXCEPT abac_tut_hr_team, abac_tut_admins
FOR TABLES
MATCH COLUMNS (
  has_tag_value('abac_tut_domain', 'hr')
  AND has_tag_value('abac_tut_sensitivity', 'internal')
) AS m
ON COLUMN m

Row Filtering + Column Masking Together

Region-based row filters run alongside domain column masks. A US team member in the HR group sees only US rows, and within those rows sees HR columns unmasked but other domains masked.

💡

Multi-Group Users Get the Union of Exemptions

A user in both abac_tut_hr_team AND abac_tut_finance_team is exempt from HR policies AND Finance policies — they see both domains unmasked. No special configuration needed; ABAC handles this automatically.

Query results — EU team (no domain group)

SELECT * FROM employee_records — running as eu_team member
+---+-------------+--------------+-----+--------------+-----------+--------------+------+
|id |employee_name|ssn |email|customer_list |cost_center|salary_band |region|
+---+-------------+--------------+-----+--------------+-----------+--------------+------+
|3 |C*** |***REDACTED***|c*** |***REDACTED***|C*** |***REDACTED***|eu |
|4 |D*** |***REDACTED***|d*** |***REDACTED***|C*** |***REDACTED***|eu |
+---+-------------+--------------+-----+--------------+-----------+--------------+------+
2 rows (region filter) · all domain columns masked (not in any domain group)

Query results — HR team (US region)

SELECT * FROM employee_records — running as hr_team + us_team member
+---+---------------+-----------+-----+--------------+-----------+--------------+------+
|id |employee_name |ssn |email|customer_list |cost_center|salary_band |region|
+---+---------------+-----------+-----+--------------+-----------+--------------+------+
|1 |Alice Johnson |123-45-6789|a*** |***REDACTED***|C*** |***REDACTED***|us |
|2 |Bob Smith |234-56-7890|b*** |***REDACTED***|C*** |***REDACTED***|us |
|5 |Eva Martinez |567-89-0123|e*** |***REDACTED***|C*** |***REDACTED***|us |
+---+---------------+-----------+-----+--------------+-----------+--------------+------+
HR columns visible · Finance + Marketing columns masked · EU rows filtered out

Why This Pattern Scales

Adding a new domain (e.g., Legal) requires only:

  1. Add legal to the abac_tut_domain governed tag values
  2. Create group abac_tut_legal_team
  3. Add two policies: mask_internal_legal, mask_confidential_legal
  4. Tag the new columns with domain=legal + sensitivity=internal/confidential

Zero changes to existing policies, UDFs, or other domain configurations.


📡

Telco — FCC CPNI Compliance

47 U.S.C. § 222 — Customer Proprietary Network Information protection

Telecommunications providers must protect CPNI — call records, data usage, and billing information that reveals subscriber service patterns. This example applies the same domain × sensitivity pattern across three regulated domains.

📞

CPNI Domain (FCC)

  • call_records restricted
  • data_usage_gb internal
💳

Billing Domain

  • account_number restricted
  • monthly_spend internal
🪪

Identity Domain (PII)

  • phone_number restricted
  • billing_address confidential
  • subscriber_name internal

Service-type row filtering

Each regional team sees only subscribers on their service type. The bundle team sees all service types since they support bundle customers who use both voice and data.

SQL
-- Bundle team sees all service types
CREATE OR REPLACE FUNCTION {catalog}.{schema}.service_type_filter_bundle(svc_type STRING)
RETURNS BOOLEAN
RETURN svc_type IN ('bundle', 'voice', 'data')

CREATE OR REPLACE POLICY telco_service_row_filter_bundle
ON SCHEMA {catalog}.{schema}
ROW FILTER {catalog}.{schema}.service_type_filter_bundle
TO abac_tut_apac_team
FOR TABLES
MATCH COLUMNS has_tag('telco_service_type') AS svc_col
USING COLUMNS (svc_col)
⚠️

Don't Tag the Row Filter Column with Sensitivity

service_type is used as the row filter column. If it is also tagged with a sensitivity tag, a column mask policy will apply to it — and Databricks forbids using a masked column in a row filter. Keep the row filter column tag (telco_service_type) separate from domain/sensitivity tags.

CPNI verification — EU team (data service)

SELECT * FROM subscriber_records — running as eu_team (data service)
+-------------+---------------+------------+--------------+------------+-------------+--------------+-------------+
|subscriber_id|subscriber_name|phone_number|account_number|service_type|data_usage_gb|call_records |monthly_spend|
+-------------+---------------+------------+--------------+------------+-------------+--------------+-------------+
|2 |B*** |***-***-0202|ACCT-****-2-BD|data |0.0 |***REDACTED***|0.0 |
|5 |E*** |***-***-0505|ACCT-****-5-EH|data |0.0 |***REDACTED***|0.0 |
|8 |H*** |***-***-0808|ACCT-****-8-HK|data |0.0 |***REDACTED***|0.0 |
+-------------+---------------+------------+--------------+------------+-------------+--------------+-------------+

Expected results by team

TeamRowsphone_numberaccount_numbercall_recordsmonthly_spend
abac_tut_adminsAll 8FullFullFullFull
abac_tut_us_teamVoice (1,4,7)***-***-XXXXACCT-****-XXXX***REDACTED***0.0
abac_tut_hr_teamBy serviceFullACCT-****-XXXX***REDACTED***0.0
abac_tut_finance_teamBy service***-***-XXXXFull***REDACTED***Full
abac_tut_marketing_teamBy service***-***-XXXXACCT-****-XXXXFull0.0

Key Learnings

  • 2D tagging (domain × sensitivity) is the core pattern — AND conditions in MATCH COLUMNS guarantee exactly one policy per column.
  • Never tag the row filter column with a sensitivity tag — Databricks forbids masked columns in row filter USING COLUMNS.
  • DOUBLE columns need DOUBLE-returning mask functions — use ALTER TABLE SET MASK mask_double for numeric columns.
  • Multi-group membership is additive — a user in two domain groups sees both domains unmasked automatically.
  • Scaling is additive — new domains only require new policies and group, never changes to existing ones.