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.
-- 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 |
-- 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)
Query results — HR team (US region)
Why This Pattern Scales
Adding a new domain (e.g., Legal) requires only:
- Add
legalto theabac_tut_domaingoverned tag values - Create group
abac_tut_legal_team - Add two policies:
mask_internal_legal,mask_confidential_legal - 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.
-- 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)
Expected results by team
| Team | Rows | phone_number | account_number | call_records | monthly_spend |
|---|---|---|---|---|---|
| abac_tut_admins | All 8 | Full | Full | Full | Full |
| abac_tut_us_team | Voice (1,4,7) | ***-***-XXXX | ACCT-****-XXXX | ***REDACTED*** | 0.0 |
| abac_tut_hr_team | By service | Full | ACCT-****-XXXX | ***REDACTED*** | 0.0 |
| abac_tut_finance_team | By service | ***-***-XXXX | Full | ***REDACTED*** | Full |
| abac_tut_marketing_team | By service | ***-***-XXXX | ACCT-****-XXXX | Full | 0.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_doublefor 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.