列篩選與欄遮罩的常見模式

本頁說明實施 ABAC 列過濾器與欄遮罩政策的常見模式。

投射相容的遮罩函數

Azure Databricks 會自動將遮蔽函數的輸出轉換,使其符合目標欄位的資料類型。 請參閱 針對欄位遮罩的自動類型轉換。

以下圖案有助於你設計與鑄造相容的遮罩功能。

回傳一個可轉換型別

遮罩欄位時,回傳相同資料型態或可投射到該欄位的型別。 檢查你政策目標欄位的資料型別,並確認函式的每個分支都回傳相容的值。

-- Succeeds: Masks a DOUBLE column, returns DOUBLE in every branch
CREATE FUNCTION mask_salary(salary DOUBLE, user_role STRING)
RETURNS DOUBLE
RETURN CASE
  WHEN user_role IN ('admin', 'hr') THEN salary
  WHEN user_role = 'manager' THEN ROUND(salary / 1000) * 1000
  ELSE 0.0
END;

-- Fails: 'CONFIDENTIAL' cannot be cast to a DOUBLE column type
CREATE FUNCTION mask_salary_as_text(salary DOUBLE, user_role STRING)
RETURNS STRING
RETURN CASE
  WHEN user_role IN ('admin', 'hr') THEN CAST(salary AS STRING)
  ELSE 'CONFIDENTIAL'
END;

避免數值溢位

當遮罩函式接受並回傳比目標欄位更寬的數值類型時,結果會自動回復到該欄位的類型。 若回傳值超過較窄型別的範圍,類型轉換會造成溢位,導致查詢在執行時失敗。

-- The target column is TINYINT (max 127). The input is upcast to BIGINT
-- for the function. Adding 1000 produces a BIGINT result that overflows
-- when cast back to TINYINT.
CREATE FUNCTION mask_score(score BIGINT)
RETURNS BIGINT
RETURN score + 1000;

多欄位類型使用 VARIANT

參見 「用單一函數遮罩多欄類型」。

試鑄相容性

測試不同資料模式的遮罩函式。

SELECT CAST(mask_salary(salary, 'admin') AS DOUBLE) FROM employees;
SELECT CAST(mask_salary(salary, 'manager') AS DOUBLE) FROM employees;
SELECT CAST(mask_salary(salary, 'viewer') AS DOUBLE) FROM employees;

用單一函式遮罩多種欄位類型

單一接受並傳回 VARIANT 的資料遮罩 UDF,可遮罩多種資料類型的資料欄,從而減少您需要維護的 UDF 和原則數量。 Azure Databricks 會先在函式執行前將欄位值轉換為 VARIANT,然後再依照 ANSI SQL 規則將回傳值轉換回該欄位的型別。

在函式內部,使用 schema_of_variant() 檢查該值,並根據其型別進行分支處理。 在每個分支中,回傳一個可投射到目標欄位型別的值。

遮罩多種數字類型

最簡單的 VARIANT 遮罩會傳回單一常數,Azure Databricks 會將其轉換為每個欄位的類型。 使用此函數的單一政策即可遮蔽 INT、DOUBLE 和 DECIMAL 欄位,而無須為每種精度分別建立函數:

CREATE FUNCTION mask_numeric(val VARIANT)
RETURNS VARIANT
DETERMINISTIC
RETURN 0::VARIANT;

若要依類型變更遮罩值,請根據 schema_of_variant() 進行分支,並為每種類型回傳適當的值:

CREATE FUNCTION flexible_mask(data VARIANT)
RETURNS VARIANT
RETURN CASE
  WHEN schema_of_variant(data) = 'BIGINT' THEN 0::VARIANT
  WHEN schema_of_variant(data) = 'DATE' THEN DATE'1970-01-01'::VARIANT
  WHEN schema_of_variant(data) = 'DOUBLE' THEN 0.00::VARIANT
  ELSE NULL::VARIANT
END;

整數型別在 VARIANT 中會擴大為 BIGINT,因此應根據 BIGINT 進行分支判斷,而不是根據 INT 或 TINYINT。

遮蔽 STRUCT、ARRAY 和 MAP 欄位

此 VARIANT 方法同時遮蔽複數型欄位,將 上述數值範例 擴展至巢狀資料。 STRUCT、ARRAY 或 MAP 欄位會以 VARIANT 的形式傳遞給函式,而 Azure Databricks 會將函式回傳的任何值轉換回該欄位宣告的型別。 回傳的值必須具有與輸入相同的結構描述,否則轉型會失敗,查詢也會出錯。

Note

Databricks Runtime 18.1 及以上版本支援 STRUCT 欄位。 ARRAY 和 MAP 欄位在 Databricks Runtime 19 及以上版本中受支援。 此casting僅能在ABAC欄位遮罩與列過濾策略中運作,無法在一般SQL或資料表層級遮罩中運作。

以下範例函數遮蔽特定 STRUCT、 ARRAY及 MAP 類型。 為每個需要遮罩的結構加入一個 WHEN 分支:

CREATE OR REPLACE FUNCTION generic_mask(val VARIANT)
RETURNS VARIANT
RETURN CASE
  -- STRUCT<id: INT, ssn: STRING>: keep id, redact ssn
  WHEN schema_of_variant(val) = 'OBJECT<id: BIGINT, ssn: STRING>' THEN
    to_variant_object(named_struct('id', val:id, 'ssn', 'xxx-xx-xxxx'))
  -- ARRAY<STRING>: return a single redacted element
  WHEN schema_of_variant(val) = 'ARRAY<STRING>' THEN
    to_variant_object(array('redacted'))
  -- MAP<STRING, STRING>: redact every value
  WHEN schema_of_variant(val) = 'OBJECT<key1: STRING, key2: STRING>' THEN
    to_variant_object(map('key1', 'redacted', 'key2', 'redacted'))
  ELSE NULL::VARIANT
END;

每個 WHEN 分支有兩項功能,詳述如下:

  1. 辨識變異結構。 每個 VARIANT schema 都需要各自的遮蔽邏輯,因此函式會根據 schema_of_variant(val) 回傳的 schema 字串進行分支,對每一種 schema 套用正確的隱去處理。
  2. 重建遮罩值。 建立符合該欄位型別的遮蔽值,並將其包裝在 to_variant_object() 中以回傳 VARIANT。

其結構描述不符合任何分支的值會落入 ELSE。

識別變體結構

比對值的 VARIANT 結構描述,而不是欄位宣告的型別:轉換為 VARIANT 會將資料正規化,因此其結構描述可能與欄位定義不同。 以下範例展示了常見案例:

欄位類型 schema_of_variant() 結果 Notes
ARRAY<STRING> ARRAY<STRING>
ARRAY<INT> ARRAY<BIGINT> 整數型態會展寬至 BIGINT。
STRUCT<id: INT, name: STRING> OBJECT<id: BIGINT, name: STRING> STRUCT 而 MAP 欄位 都變成 OBJECT。
STRUCT<name: STRING, id: INT> OBJECT<id: BIGINT, name: STRING> 欄位依照鍵字母順序排序,而非宣告順序。
ARRAY<STRUCT<id: INT, name: STRING>> ARRAY<OBJECT<id: BIGINT, name: STRING>>
MAP<STRING, STRING> 判決 {a: 'x'} OBJECT<a: STRING> 依列而異:每列的鍵決定結構。
MAP<STRING, STRING> 判決 {a: 'x', b: 'y'} OBJECT<a: STRING, b: STRING> 同一欄的不同行會產生不同的結構。
MAP<STRING, STRING> 判決 {a: null} OBJECT<a: VOID> 空值變為 VOID。
MAP<STRING, STRUCT<id: INT, name: STRING>> 判決 {a: ...} OBJECT<a: OBJECT<id: BIGINT, name: STRING>, ...>

重建遮罩值

對於每個符合的 schema,建立一個具有相同結構的遮蔽值,然後再以 to_variant_object() 包住:

  • 用 named_struct() 來重建一個 STRUCT,保留你想要的欄位,並替換剩下的欄位。
  • 用 array() 來重建一個 ARRAY.
  • 使用 map() 重建具有經過遮蔽之值的 MAP。

Azure Databricks 會將傳回的 VARIANT 轉換回資料行宣告的類型,因此每個重建的值都必須可轉換為該類型。

測試 VARIANT 遮罩

在你將函式附加到政策上之前,可以在純查詢中測試它,以確認遮蔽輸出。 以下函式會遮罩欄位 ARRAY<STRUCT<id: BIGINT, value: FLOAT>> ,並對其他結構產生錯誤:

CREATE OR REPLACE FUNCTION mask_points(v VARIANT)
RETURNS VARIANT
RETURN CASE
  WHEN schema_of_variant(v) = 'ARRAY<OBJECT<id: BIGINT, value: FLOAT>>' THEN
    to_variant_object(array(named_struct('id', 1, 'value', 2.1)))
  ELSE raise_error('Unexpected VARIANT schema: ' || schema_of_variant(v))
END;

使用 to_variant_object() 轉換欄位、套用遮罩函數,然後使用 variant_get() 將經遮罩處理的 VARIANT 轉換回該欄位的類型。 這與執行時政策的運作方式相呼應:

SELECT variant_get(mask_points(to_variant_object(points)), '$', typeof(points)) AS masked
FROM my_catalog.my_schema.my_table;

Limitations

  • 包含 CHAR、VARCHAR、GEOMETRY、GEOGRAPHY 或 TIME 的複雜欄位無法使用 VARIANT 進行遮罩。
  • MAP 鍵必須是 STRING。 輸入為 MAP<INT, ...>、MAP<DATE, ...> 等的欄位不會被轉換。

在敏感欄位被標記前,請阻止存取

一種常見的治理模式是根據資料是否被分類來控制存取。 你可以用預設的限制性標籤和根據分類狀態執行不同保護層級的政策來實作。

  1. 預設對所有新物件套用類似 classification : unverified 標籤,無論是透過自動化,或透過在目錄或結構層級套用標籤繼承,使新增到目錄或結構的任何資料表都能自動繼承該標籤。
  2. 建立一個列過濾器政策,阻擋對標記為 classification : unverified的資料表的存取。
  3. 建立欄位遮罩政策,遮蔽標籤已不存在的資料表 classification : unverified 中敏感欄位。
  4. 當資料管理員完成分類後,他們會更新標籤。 封鎖政策不再匹配,掩蔽政策生效。
-- Block access to unverified tables for all non-admin users
CREATE FUNCTION catalog.schema.block_all() RETURNS BOOLEAN
  RETURN FALSE;

CREATE POLICY block_unverified
ON CATALOG my_catalog
ROW FILTER catalog.schema.block_all
TO `account users` EXCEPT `data_admins`
FOR TABLES
WHEN has_tag_value('classification', 'unverified');

為了保護機密資料,在資料被分類之後,請定義一項「欄位遮罩政策」,當 classification : unverified 標籤不再存在時便會生效。

CREATE FUNCTION catalog.schema.mask_pii(val STRING)
RETURNS STRING
RETURN '***';

CREATE POLICY mask_reviewed_pii
ON CATALOG my_catalog
COLUMN MASK catalog.schema.mask_pii
TO `account users`
EXCEPT `data_admins`
FOR TABLES
WHEN NOT has_tag_value('classification', 'unverified')
MATCH COLUMNS (has_tag_value('pii', 'name') OR has_tag_value('pii', 'address')) AS m
ON COLUMN m;

無正則表達式的部分揭示

用字串運算而非正則表達式來揭示敏感值的一部分。 基於正則表達式的遮罩會掃描每一列的整個值,這在大型文字欄位上成本較高(參見 避免在大型文字欄位使用正則表達式遮蔽)。

CREATE FUNCTION mask_ssn(ssn STRING, show_last INT) RETURNS STRING
DETERMINISTIC
  RETURN CONCAT('***-**-', RIGHT(ssn, show_last));

一致性雜湊(確定性假名化)

一致雜湊(也稱為確定性假名化)以多個資料表間相同的雜湊值取代敏感資料。 將函式標記為 可以 DETERMINISTIC 讓引擎知道該函式對相同輸入總是回傳相同結果,有助於優化查詢。 參見 使用確定性、安全錯誤表達式。

以下函式會持續雜湊字串值,並使用 version 參數支援金鑰旋轉。 透過政策條款version遞增該USING COLUMNS數字,以產生新的雜湊值,且不會破壞使用先前版本的歷史資料。 該函式在雜湊前將原始值與版本號串接,因此相同輸入與版本的雜湊值總是相同。

CREATE FUNCTION pseudonymize(val STRING, version INT) RETURNS STRING
DETERMINISTIC
  RETURN SHA2(CONCAT(val, CAST(version AS STRING)), 256);

根據查詢使用者的屬性來遮罩欄位

Important

ABAC 政策中的身份屬性仍處於 Beta 階段。 要使用這些工具,帳號管理員必須:

欄位遮罩政策可利用查詢使用者的身份屬性來遮蔽敏感資料,而無需專用群組。 例如,系統可對具有 department = HR 的使用者不遮罩資料,並對其他所有人遮罩資料。

這些模式要求從身分識別提供者為使用者佈建身分屬性,而這些函式的行為與僅含標籤的條件不同,因此會影響你撰寫政策的方式。 在使用它們之前,先複習 Identity 屬性函式 和 identity 屬性。

Important

當使用者的該屬性沒有值,或屬性鍵不存在時,函式會解析為 false。 寫條件使此 false 結果限制存取,而非授予存取權限。 使用 NOT 來否定比對,這樣除非屬性符合比對條件,否則會套用遮罩。 例如,WHEN NOT has_identity_attribute_value('department', 'HR') 會遮蔽該欄,除了部門為 HR 的使用者之外;而且由於缺失值也屬於 false,因此沒有部門屬性值的使用者也會被遮蔽。 避免相反的情況:只在該屬性相符時才進行遮蔽的條件,會讓沒有該屬性值的使用者未被遮蔽。

關於評估行為,請參見 身份屬性條件。 關於限制,請參見 政策條件中的身份屬性。

匹配固定值

遮罩 ssn 對所有部門不是 HR 的人員:

CREATE FUNCTION hr_catalog.people.mask_ssn(s STRING) RETURNS STRING RETURN '***-**-****';

CREATE OR REPLACE POLICY mask_ssn_non_hr
ON SCHEMA hr_catalog.people
COLUMN MASK hr_catalog.people.mask_ssn
TO `account users`
FOR TABLES
WHEN NOT has_identity_attribute_value('department', 'HR')
MATCH COLUMNS has_tag_value('pii', 'ssn') AS ssn_col
ON COLUMN ssn_col;

在這個例子中,部門為 HR 的使用者會看到實際值。 屬於任何其他部門的使用者和沒有部門屬性的使用者都會看到該遮罩。

與受控標籤比對

前一個例子在政策中指定了特定的屬性值(HR),因此要涵蓋多個部門,就必須為每個部門撰寫獨立的政策。 要用單一政策涵蓋所有部門,請將每個資料表標註擁有該部門的部門,然後將查詢使用者的 department 屬性與該標籤進行比較。 該欄位僅在使用者部門與 dept_tag 表格值相符時才會揭露:

CREATE FUNCTION prod.sales.mask_ssn(s STRING) RETURNS STRING RETURN '***-**-****';

CREATE OR REPLACE POLICY mask_unless_dept_matches
ON SCHEMA prod.sales
COLUMN MASK prod.sales.mask_ssn
TO `account users`
FOR TABLES
WHEN NOT has_identity_attribute_tag_match('department', 'dept_tag')
MATCH COLUMNS has_tag_value('pii', 'ssn') AS ssn_col
ON COLUMN ssn_col;

屬性鍵與值皆以大小寫區分,且值會被精確比較: Finance 且 finance 不匹配。

限制代表使用者行動的外部代理的存取權限

Important

ABAC 政策中的上下文屬性仍處於 Beta 階段。 要使用這些工具,帳號管理員必須從帳號主控台的預覽頁面啟用 UC ABAC 上下文屬性預覽。 請參閱 管理帳戶層級預覽。

上下文屬性可用來限制代表使用者透過 OAuth 應用程式發出的請求對資料的存取(使用者對機器 (U2M) 授權)。 如果代理是透過 OAuth 連接,這種設定可以用來防止他們在代表使用者行動時存取資料,即使使用者在工作區直接查詢資料時仍能讀取資料。

任何經過 OAuth 認證的 Azure Databricks CLI、SDK 或 SQL Statement Execution API 的存取,即使使用者手動查詢,也會設定request.is_on_behalf_of為 'true'。 使用個人存取權杖(PAT)認證的存取則不然。 無法透過此機制擷取 Genie 存取資料。

這些模式使用上下文屬性函式。 關於可用的屬性與行為,請參見上下文屬性函數(Beta)。

將代理程式連線至 Azure Databricks

要使用上下文屬性,請使用 自訂的 OAuth 應用程式連接代理:

  1. 帳號管理員會從帳號主控台啟用 UC ABAC 情境屬性 預覽。 請參閱 管理 Azure Databricks 預覽。
  2. 帳號管理員會在帳號主控台註冊一個自訂的 OAuth 應用程式,並記錄其客戶端 ID。
  3. 透過該 OAuth 應用程式將代理程式連接到 Azure Databricks 管理的 MCP。 請參閱 使用 OAuth 認證的 Connect 用戶端。

使用內建客戶端的代理程式仍透過 databricks-cli OAuth 進行認證,因此 request.is_on_behalf_of 讀取 'true'。 然而,你無法區分它的請求和手動使用 CLI,因為兩者共用 databricks-cli 客戶端 ID。 要管理特定應用程式,註冊一個自訂的 OAuth 應用程式,並透過它連接代理程式。

Warning

確保客服無法透過保單不涵蓋的路徑取得資料:

  • 如果你是根據 request.is_on_behalf_of 來限制存取,請確保代理程式無法使用 PAT 進行驗證。 PAT 不會設 request.is_on_behalf_of 為 'true',因此該屬性的條件不會限制它。
  • 如果你是根據 request.client_id 來限制存取,請確保代理程式無法透過你的條件未涵蓋的用戶端進行連線,例如通用的 databricks-cli 用戶端。

為代表請求遮蔽欄位

對於代表使用者執行的請求(例如透過已註冊的 OAuth 應用程式運作的代理程式),請遮罩 ssn;而對於直接查詢,則維持不遮罩:

CREATE FUNCTION hr_catalog.people.mask_ssn(s STRING) RETURNS STRING RETURN '***-**-****';

CREATE OR REPLACE POLICY mask_ssn_for_agents
ON SCHEMA hr_catalog.people
COLUMN MASK hr_catalog.people.mask_ssn
TO `account users`
FOR TABLES
WHEN has_context_attribute_value('request.is_on_behalf_of', 'true')
MATCH COLUMNS has_tag_value('pii', 'ssn') AS ssn_col
ON COLUMN ssn_col;

在此範例中,直接查詢會傳回實際值,而代表他人提出的要求則會看到經遮罩的值。 透過 CLI 和 SQL Statement Execution API,request.is_on_behalf_of 也會讀取 'true',因此此原則也會遮蔽這些請求中的該欄位。 若要改為鎖定特定應用程式,請將 request.client_id 與該應用程式的用戶端 ID 比對。

將欄位限制為已核准的申請

對除您已核准的應用程式(以 OAuth 用戶端 ID 識別)以外的所有外部請求都進行遮罩 ssn :

CREATE OR REPLACE POLICY mask_ssn_unapproved_apps
ON SCHEMA hr_catalog.people
COLUMN MASK hr_catalog.people.mask_ssn
TO `account users`
FOR TABLES
WHEN NOT has_context_attribute_value('request.client_id', '<your-app-client-id>')
MATCH COLUMNS has_tag_value('pii', 'ssn') AS ssn_col
ON COLUMN ssn_col;

要查看是哪個應用程式提出請求,請檢查稽核日誌中的該identity_metadata.acting_resource欄位。

使用僅根據欄位條件的列篩選

使用僅參考資料表欄位的簡單布林邏輯過濾資料列。 僅欄謂詞能啟用謂詞下推,讓引擎在掃描時能跳過無關資料(參見 「理解受保護資料表上的謂詞下推」)。

CREATE FUNCTION filter_by_region(region STRING, allowed STRING)
RETURNS BOOLEAN
DETERMINISTIC
  RETURN array_contains(split(allowed, ','), lower(region));

使用一個能將允許區域作為常數的策略:

CREATE POLICY regional_access
ON CATALOG analytics
ROW FILTER filter_by_region
TO 'emea_team'
FOR TABLES
MATCH COLUMNS has_tag('region') AS rgn
USING COLUMNS (rgn, 'emea,apac');

跨多個相關欄位的列篩選

當一個資料表有多欄代表相關屬性(例如, ship_to_country 和 bill_to_country),你可以用不同的標籤條件匹配它們,並將兩者都傳遞給同一個 UDF。 這樣可以避免為每欄建立獨立的政策。 政策在條款 MATCH COLUMNS 中最多可包含三欄表達式(參見 政策配額)。

CREATE FUNCTION filter_by_countries(ship_country STRING, bill_country STRING, allowed STRING)
RETURNS BOOLEAN
DETERMINISTIC
  RETURN array_contains(split(allowed, ','), lower(ship_country))
      OR array_contains(split(allowed, ','), lower(bill_country));

CREATE POLICY regional_orders
ON SCHEMA prod.orders
ROW FILTER filter_by_countries
TO analysts
FOR TABLES
WHEN has_tag_value('sensitivity', 'high')
MATCH COLUMNS
  has_tag('ship_country') AS ship,
  has_tag('bill_country') AS bill
USING COLUMNS (ship, bill, 'us,ca,mx');

分析師只會看到出貨國或帳單國在允許清單中的訂單。

ABAC 政策 UDF 中的查詢表

當存取規則因使用者而異且無法僅透過政策 TO/EXCEPT 條款表達時,你可以用一個小型查詢表來檢查存取權限。 盡可能使用 TO/EXCEPT ,因為這是鎖定主要目標的首選方法(參見「 針對主要目標的方法」)。 保持查詢表小,這樣優化器才能將子查詢轉換為廣播雜湊聯接(參見 保持查詢表小)。

CREATE TABLE access_rules (
  principal VARCHAR(255),
  priority VARCHAR(64)
);

INSERT INTO access_rules VALUES
  ('alice@company.com', '1-URGENT'),
  ('alice@company.com', '2-HIGH'),
  ('bob@company.com', '1-URGENT');

CREATE FUNCTION priority_allowed(o_priority STRING) RETURNS BOOLEAN
RETURN EXISTS (
  SELECT 1 FROM access_rules
  WHERE principal = session_user() AND priority = o_priority
);

CREATE POLICY priority_filter
ON CATALOG operations
ROW FILTER priority_allowed
TO `account users`
FOR TABLES
MATCH COLUMNS has_tag('priority') AS pri
USING COLUMNS (pri);