ABAC 策略的 UDF 最佳做法

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

为什么 UDF 性能很重要

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

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

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

ABAC UDF 的黄金规则

  • 保持简单:支持基本 CASE 语句和清除布尔表达式。
  • 保持确定性:相同的输入应始终生成相同的输出来缓存和一致的联接。
  • 避免外部调用:没有对其他数据库的 API 调用或查找。
  • 仅使用 UDF 中的内置函数:不要从 UDF 中调用其他 UDF。
  • 大规模测试:验证至少 100 万行的性能。
  • 尽量减少复杂性:避免多级嵌套和不必要的函数调用。
  • 仅列引用:仅引用目标表的列以启用下推。

要避免的常见反模式

  • 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 性能最佳做法:简单的逻辑、确定性行为、没有外部调用,并且只使用内置函数。

列掩码:快速确定性

-- Deterministically pseudonymize patient_id with a version tag for rotation.
CREATE OR REPLACE FUNCTION mask_patient_id_fast(patient_id STRING)
RETURNS STRING
DETERMINISTIC
RETURN CONCAT('REF_', SHA2(CONCAT(patient_id, ':v1'), 256));

为什么这样工作:

  • 简单 CASE 语句
  • 无外部依赖项
  • 一致联接的确定性结果
  • 使主体逻辑远离 UDF

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

-- 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;

为什么这样工作:

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

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

-- 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));

为什么这样工作:

  • 跨掩码数据集保留联接
  • 包括版本标记(:v1),以支持密钥轮换,而无需有意破坏历史数据

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

-- 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;