ABAC 策略的 UDF 最佳做法

重要

此功能目前以公共预览版提供。

本页演示如何编写高性能用户定义函数(UDF),以便在 Unity 目录中的 ABAC 行筛选器和列掩码策略中使用。

为什么 UDF 性能很重要

ABAC 策略针对每个适用的行或列 的每个查询执行 运行。 低效 UDF 可以:

  • 阻止并行执行和谓词下推
  • 强制每行的昂贵联接或查找
  • 将查询时间从毫秒增加到秒(或更多)
  • 中断缓存和矢量化
  • 使分析和报告不可靠

在生产环境中,这会使数据管理成为瓶颈,而不是启用器。

ABAC UDF 的黄金规则

  • 保持简单:支持基本 CASE 语句和清除布尔表达式。
  • 保持确定性:相同的输入应始终生成相同的输出来缓存和一致的联接。
  • 避免外部调用:没有对其他数据库的 API 调用或查找。
  • 仅使用 UDF 中的内置函数:不要从 UDF 中调用其他 UDF。
  • 大规模测试:验证至少 100 万行的性能。
  • 尽量减少复杂性:避免多级嵌套和不必要的函数调用。
  • 仅列引用:仅引用目标表的列以启用下推。
  • 首选 SQL 而不是 Python UDF:以提高性能。 如果必须使用 Python,请在 UDF 不涉及非确定性逻辑和依赖项的情况下显式标记为 DETERMINISTIC

要避免的常见反模式

  • UDF 中的外部 API 调用:导致网络延迟、超时和单一故障点。
  • 复杂的子查询或联接:强制嵌套循环联接并防止优化。
  • 大型文本上的重正则表达式:每行 CPU 和内存密集型。
  • 元数据查找:避免按行检查命中元数据或标识源,例如information_schema查询、用户配置文件查找或is_member()is_account_group_member()。 为每个记录添加额外的扫描。
  • 动态 SQL 生成:无查询优化并阻止缓存。
  • 非确定性逻辑:防止缓存和一致联接,请参阅 非确定性逻辑的影响

在策略中保留访问检查,而不是 UDF

常见的错误是调用 is_account_group_member() 或 is_member() 直接在 UDF 内部。 这使得函数变慢,并使 UDF 更易于重用。

相反,请遵循以下模式:

  • UDF 角色:仅关注如何转换、屏蔽或筛选数据。 仅使用传入它的列和参数。
  • 策略角色:定义谁(主体、组)以及何时(标记)UDF 应通过引用 ABAC 策略中的主体来应用。

非确定性逻辑的影响

某些方案(如用于研究的随机掩码)每次都需要不同的输出。
如果必须使用非确定性函数:

  • 由于没有缓存,预期性能会降低。
  • JOIN 可能会失败或返回不一致的结果。
  • 报表可能会显示运行之间的不同数据。
  • 故障排除和验证可能更难。

UDF 示例

下面是用于列掩码和行筛选的生产友好模式。 所有示例都遵循 ABAC 性能最佳做法:简单的逻辑、确定性行为、没有外部调用,并且只使用内置函数。

列掩码:使用版本控制进行确定性假名化

-- Create a consistent pseudonymized reference ID.
CREATE OR REPLACE FUNCTION mask_id_deterministic(id STRING)
RETURNS STRING
DETERMINISTIC
RETURN CONCAT('REF_', SHA2(CONCAT(id, ':v1'), 256));

为什么这样工作:

  • 简单 CASE 语句
  • 无外部依赖项
  • 一致联接的确定性结果
  • 将核心逻辑保留在 UDF 之外
  • 跨掩码数据集保留联接
  • 包括版本标记(:v1),以支持密钥轮换,而无需有意破坏历史数据

列掩码:不带正则表达式热点的部分显示

-- Reveal only the last 4 digits of an SSN, masking the rest.
CREATE OR REPLACE FUNCTION mask_ssn_last4(ssn STRING)
RETURNS STRING
DETERMINISTIC
RETURN CASE
  WHEN ssn IS NULL THEN NULL
  WHEN LENGTH(ssn) >= 4 THEN CONCAT('XXX-XX-', RIGHT(REGEXP_REPLACE(ssn, '[^0-9]', ''), 4))
  ELSE 'MASKED'
END;

为什么这样工作:

  • 使用单个轻型正则表达式去除非数字
  • 避免在大型文本字段中传递多个正则表达式

行筛选器:按区域对下推友好

-- Returns TRUE if the row's state is in the allowed set.
CREATE OR REPLACE FUNCTION filter_by_region(state STRING, allowed ARRAY<STRING>)
RETURNS BOOLEAN
DETERMINISTIC
RETURN array_contains(TRANSFORM(allowed, x -> lower(x)), lower(state));

为什么这样工作:

  • 简单的布尔逻辑
  • 仅引用表列
  • 启用谓词下推和向量化

行筛选器:确定性多条件

-- Returns TRUE if the row's region is in the allowed set.
CREATE OR REPLACE FUNCTION filter_region_in(region STRING, allowed_regions ARRAY<STRING>)
RETURNS BOOLEAN
DETERMINISTIC
RETURN array_contains(TRANSFORM(allowed_regions, x -> lower(x)), lower(region));

为什么这样工作:

  • 支持一个函数中的多个角色和地理位置
  • 保持逻辑平面,以便更好地优化

测试 UDF 性能

使用综合规模测试在生产之前验证行为和性能。 例如:

WITH test_data AS (
  SELECT
    patient_id,
    your_mask_function(patient_id) as masked_id,
    current_timestamp() as start_time
  FROM (
    SELECT CONCAT('PAT', LPAD(seq, 6, '0')) as patient_id
    FROM range(1000000)  -- 1 million test rows
  )
)
SELECT
  COUNT(*) as rows_processed,
  MAX(start_time) - MIN(start_time) as total_duration,
  COUNT(*) / EXTRACT(EPOCH FROM (MAX(start_time) - MIN(start_time))) as rows_per_second
FROM test_data;

ABAC 列掩码类型转换

Databricks 自动将从 ABAC 策略解析出的列掩码函数的输出转换,以匹配目标列的数据类型。 这可确保屏蔽列时类型一致性和可靠的查询行为。

自动类型转换的工作原理

  1. 策略评估:ABAC 策略确定是否应应用掩码。
  2. 掩码函数执行:如果需要掩码,掩码函数将执行。
  3. 自动强制转换:掩码函数的结果会自动转换为目标列的数据类型。
  4. 结果返回:正确键入的结果将返回到查询。

所有类型转换行为均遵循 ANSI SQL 标准进行 CAST 操作。 有关完整兼容性详细信息,请参阅 “返回”。

类型安全掩码的最佳做法

遵循这些模式,确保您的掩码函数在自动类型转换下可靠工作,并避免运行时故障。

设计类型一致性掩码函数

确保掩码函数返回与目标列兼容的类型:

-- Successful: Returns same type as target column
CREATE FUNCTION safe_salary_mask(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;

-- Unsuccessful: Returns different type that might not cast
CREATE FUNCTION risky_salary_mask(salary DOUBLE, user_role STRING)
RETURNS STRING
RETURN CASE
  WHEN user_role IN ('admin', 'hr') THEN CAST(salary AS STRING)
  ELSE 'CONFIDENTIAL'  -- This will fail casting to DOUBLE
END;

使用 VARIANT 进行灵活的掩码处理

对于具有不同输出类型的复杂掩码方案,用于 VARIANT 适应不同的数据类型:

CREATE FUNCTION flexible_mask(data VARIANT)
RETURNS VARIANT
RETURN CASE
  WHEN schema_of_variant(data) = 'INT' 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;

使用各种数据方案进行测试

在生产部署之前,使用不同数据模式和用户上下文测试掩码函数:

-- Test different access levels using different users/groups
SELECT * FROM sensitive_table;

-- Test casting behavior for different regional scenarios
SELECT CAST(mask_transaction(transaction_info, 'US') AS STRUCT<id INT, amount DOUBLE>);
SELECT CAST(mask_transaction(transaction_info, 'JP') AS STRUCT<id INT, amount DOUBLE>);
SELECT CAST(mask_transaction(transaction_info, 'CN') AS STRUCT<id INT, amount DOUBLE>);

局限性

有关行文件文件和列掩码的限制,请参阅“限制”。