返回博客

策略即查询重写:将权限下推到数据层

传统的权限控制在应用层逐行过滤数据,面对百万级数据集时性能灾难性下降。"策略即查询重写"(Policy as Query Rewrite)将权限策略编译为 SQL WHERE 子句,直接下推到数据库引擎执行,实现行级(Row-Level)和列级(Column-Level)的数据访问控制。本文详解策略编译器、SQL 注入防护、多数据源适配、性能基准测试,以及与 coomia-dip 三层权限模型的集成。

Coomia发布于 2025年9月18日18 分钟阅读
分享本文Twitter / X

系列:S6 平台工程 · 第 5 篇 | 难度:高级 | 阅读时间:18 分钟

策略即查询重写:将权限下推到数据层

#TL;DR

传统的权限控制在应用层逐行过滤数据,面对百万级数据集时性能灾难性下降。"策略即查询重写"(Policy as Query Rewrite)将权限策略编译为 SQL WHERE 子句,直接下推到数据库引擎执行,实现行级(Row-Level)和列级(Column-Level)的数据访问控制。本文详解策略编译器、SQL 注入防护、多数据源适配、性能基准测试,以及与 coomia-dip 三层权限模型的集成。

#1. 为什么需要查询重写

#1.1 应用层过滤的困境

传统权限模型在应用层工作:

Code
传统模式:
  1. 用户请求 "SELECT * FROM orders"
  2. 数据库返回 100 万行
  3. 应用层逐行检查权限
  4. 过滤后返回 1000 行给用户

问题:
  - 数据库传输了 99.9% 的无用数据
  - 应用层内存占用巨大
  - 逐行权限检查 O(n) 线性增长
  - 分页、排序、聚合全部在应用层重做

#1.2 查询重写的优势

Code
查询重写模式:
  1. 用户请求 "SELECT * FROM orders"
  2. 策略引擎将请求重写为:
     "SELECT * FROM orders WHERE region IN ('east', 'south')
      AND classification <= 'C2'"
  3. 数据库只返回 1000 行授权数据
  4. 分页、排序、聚合由数据库高效执行

优势:
  - 数据库只处理授权数据
  - 利用索引加速过滤
  - 内存占用降低 99%+
  - 分页/排序/聚合原生支持

#1.3 核心架构

Code
┌──────────────┐     ┌──────────────────┐     ┌──────────────┐
│  用户查询     │────>│  策略编译器       │────>│  重写后查询   │
│  Original    │     │  Policy Compiler  │     │  Rewritten   │
│  Query       │     │                  │     │  Query       │
└──────────────┘     └────────┬─────────┘     └──────┬───────┘
                              │                       │
                     ┌────────▼─────────┐             │
                     │  策略存储         │             │
                     │  - RBAC 角色     │             │
                     │  - ABAC 属性     │             ▼
                     │  - ReBAC 关系    │     ┌──────────────┐
                     └──────────────────┘     │  数据库引擎   │
                                              │  (Doris/PG)  │
                                              └──────────────┘

#2. 策略编译器设计

#2.1 策略到 SQL 的编译管道

Python
class PolicyCompiler:
    """策略编译器:将权限策略转换为 SQL 条件"""

    def __init__(
        self,
        rbac_engine: RBACEngine,
        abac_engine: ABACEngine,
        rebac_engine: ReBACheckEngine,
        schema_registry: SchemaRegistry,
    ):
        self._rbac = rbac_engine
        self._abac = abac_engine
        self._rebac = rebac_engine
        self._schema = schema_registry

    async def compile(
        self,
        query: ParsedQuery,
        subject: AuthenticatedUser,
        context: RequestContext,
    ) -> RewrittenQuery:
        """
        编译管道:
        1. 解析原始查询,提取涉及的表和列
        2. 查找每个表的行级策略
        3. 查找每个列的列级策略
        4. 编译策略为 SQL 条件
        5. 注入 WHERE 子句和 SELECT 列过滤
        """
        # Step 1: 解析查询
        tables = query.extract_tables()
        columns = query.extract_columns()

        # Step 2: 行级策略编译
        row_filters = []
        for table in tables:
            policies = await self._get_row_policies(table, subject, context)
            for policy in policies:
                sql_condition = self._compile_policy_to_sql(policy, table)
                row_filters.append(sql_condition)

        # Step 3: 列级策略编译
        column_masks = {}
        for table, col in columns:
            mask = await self._get_column_mask(table, col, subject, context)
            if mask:
                column_masks[(table, col)] = mask

        # Step 4: 重写查询
        rewritten = self._rewrite_query(query, row_filters, column_masks)

        return RewrittenQuery(
            sql=rewritten.sql,
            params=rewritten.params,
            audit_info=AuditInfo(
                original_query=query.sql,
                applied_policies=[p.id for p in row_filters],
                applied_masks=list(column_masks.keys()),
            ),
        )

#2.2 策略表达式语言

coomia-dip 定义了一种策略表达式语言,可编译为多种数据库方言:

YAML
# 策略定义示例
policies:
  - id: "pol-region-isolation"
    name: "区域数据隔离"
    description: "用户只能看到自己区域的数据"
    target:
      object_types: ["order", "customer", "inventory"]
    condition:
      operator: "in"
      field: "resource.region"
      value_source: "subject.allowed_regions"
    effect: "FILTER"

  - id: "pol-classification-ceiling"
    name: "分类等级天花板"
    description: "用户只能看到不超过自己等级的数据"
    target:
      object_types: ["*"]
    condition:
      operator: "lte"
      field: "resource.classification_level"
      value_source: "subject.clearance_level"
    effect: "FILTER"

  - id: "pol-owner-full-access"
    name: "数据所有者完全访问"
    description: "数据的创建者有完全访问权"
    target:
      object_types: ["*"]
    condition:
      operator: "eq"
      field: "resource.owner_id"
      value_source: "subject.user_id"
    effect: "ALLOW_ALL"

  - id: "pol-time-restriction"
    name: "工作时间限制"
    description: "敏感数据仅在工作时间内可访问"
    target:
      object_types: ["sensitive_report"]
      classification_min: "B1"
    condition:
      operator: "and"
      conditions:
        - operator: "gte"
          field: "environment.hour"
          value: 9
        - operator: "lte"
          field: "environment.hour"
          value: 18
        - operator: "in"
          field: "environment.day_of_week"
          value: [1, 2, 3, 4, 5]
    effect: "FILTER"

#2.3 表达式到 SQL 的编译

Python
class ExpressionCompiler:
    """将策略表达式编译为 SQL 条件"""

    def compile_condition(
        self,
        condition: PolicyCondition,
        table_alias: str,
        subject: AuthenticatedUser,
        context: RequestContext,
    ) -> SQLFragment:
        """递归编译策略条件为 SQL 片段"""

        match condition.operator:
            case "eq":
                value = self._resolve_value(condition.value_source, subject, context)
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} = %s",
                    params=[value],
                )

            case "neq":
                value = self._resolve_value(condition.value_source, subject, context)
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} != %s",
                    params=[value],
                )

            case "in":
                values = self._resolve_value(condition.value_source, subject, context)
                placeholders = ", ".join(["%s"] * len(values))
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} IN ({placeholders})",
                    params=values,
                )

            case "not_in":
                values = self._resolve_value(condition.value_source, subject, context)
                placeholders = ", ".join(["%s"] * len(values))
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} NOT IN ({placeholders})",
                    params=values,
                )

            case "lte":
                value = self._resolve_value(condition.value_source, subject, context)
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} <= %s",
                    params=[value],
                )

            case "gte":
                value = self._resolve_value(condition.value_source, subject, context)
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} >= %s",
                    params=[value],
                )

            case "between":
                low = self._resolve_value(condition.low_source, subject, context)
                high = self._resolve_value(condition.high_source, subject, context)
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} BETWEEN %s AND %s",
                    params=[low, high],
                )

            case "like":
                pattern = self._resolve_value(condition.value_source, subject, context)
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} LIKE %s",
                    params=[pattern],
                )

            case "is_null":
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} IS NULL",
                    params=[],
                )

            case "is_not_null":
                return SQLFragment(
                    sql=f"{table_alias}.{condition.field} IS NOT NULL",
                    params=[],
                )

            case "and":
                children = [
                    self.compile_condition(c, table_alias, subject, context)
                    for c in condition.conditions
                ]
                sql = " AND ".join(f"({c.sql})" for c in children)
                params = [p for c in children for p in c.params]
                return SQLFragment(sql=f"({sql})", params=params)

            case "or":
                children = [
                    self.compile_condition(c, table_alias, subject, context)
                    for c in condition.conditions
                ]
                sql = " OR ".join(f"({c.sql})" for c in children)
                params = [p for c in children for p in c.params]
                return SQLFragment(sql=f"({sql})", params=params)

            case "not":
                child = self.compile_condition(
                    condition.conditions[0], table_alias, subject, context
                )
                return SQLFragment(sql=f"NOT ({child.sql})", params=child.params)

    def _resolve_value(
        self,
        source: str,
        subject: AuthenticatedUser,
        context: RequestContext,
    ) -> Any:
        """解析值来源"""
        if source.startswith("subject."):
            attr = source[len("subject."):]
            return getattr(subject, attr)
        elif source.startswith("environment."):
            attr = source[len("environment."):]
            return context.get_env(attr)
        elif source.startswith("constant."):
            return source[len("constant."):]
        else:
            return source  # 字面值

#3. 行级安全(Row-Level Security)

#3.1 RLS 实现架构

Code
用户查询: SELECT * FROM orders WHERE status = 'pending'

策略编译后:
  SELECT * FROM orders
  WHERE status = 'pending'                    -- 用户原始条件
    AND region IN ('east', 'south')            -- 区域隔离策略
    AND classification_level <= 3              -- 分类等级策略
    AND (owner_id = 'user123'                  -- 所有者策略
         OR department_id IN (10, 20, 30))     -- 部门可见性策略

#3.2 多策略合并逻辑

Python
class RowLevelSecurityRewriter:
    """行级安全重写器"""

    async def apply_rls(
        self,
        query: ParsedQuery,
        table: TableRef,
        subject: AuthenticatedUser,
        context: RequestContext,
    ) -> list[SQLFragment]:
        """
        应用行级安全策略。

        合并逻辑:
        - 同一策略组内的条件用 OR 合并(满足任一即可)
        - 不同策略组之间用 AND 合并(必须同时满足)
        - ALLOW_ALL 策略可跳过后续 FILTER 策略
        """
        applicable_policies = await self._find_applicable_policies(
            table.object_type, subject, context
        )

        if not applicable_policies:
            # 没有匹配策略 → 默认拒绝所有行
            return [SQLFragment(sql="1 = 0", params=[])]

        # 检查是否有 ALLOW_ALL 策略
        for policy in applicable_policies:
            if policy.effect == "ALLOW_ALL":
                condition = self._compiler.compile_condition(
                    policy.condition, table.alias, subject, context
                )
                # 如果 ALLOW_ALL 条件满足,跳过其他过滤
                # 使用 CASE WHEN 表达
                return []  # 不需要额外过滤

        # 按策略组分组
        groups: dict[str, list[SQLFragment]] = defaultdict(list)
        for policy in applicable_policies:
            compiled = self._compiler.compile_condition(
                policy.condition, table.alias, subject, context
            )
            groups[policy.group_id].append(compiled)

        # 组内 OR,组间 AND
        group_conditions = []
        for group_id, conditions in groups.items():
            if len(conditions) == 1:
                group_conditions.append(conditions[0])
            else:
                sql = " OR ".join(f"({c.sql})" for c in conditions)
                params = [p for c in conditions for p in c.params]
                group_conditions.append(SQLFragment(sql=f"({sql})", params=params))

        return group_conditions

#3.3 ReBAC 驱动的行级过滤

当 ReBAC 关系决定行可见性时,需要将关系图查询转为 SQL:

Python
class ReBACRowFilter:
    """基于 ReBAC 关系的行级过滤"""

    async def compile_rebac_filter(
        self,
        table: TableRef,
        subject: SubjectRef,
        permission: str,
    ) -> SQLFragment:
        """
        将 ReBAC 关系转为 SQL 行过滤条件。

        策略:预计算 + 物化视图
        - 定期将 ReBAC 关系展开为 (user, resource) 对
        - 存入物化视图用于 JOIN
        """
        # 方案1:使用物化视图 JOIN
        # 适用于关系相对稳定的场景
        return SQLFragment(
            sql=f"""
                {table.alias}.id IN (
                    SELECT resource_id
                    FROM rebac_materialized_permissions
                    WHERE subject_type = %s
                      AND subject_id = %s
                      AND resource_type = %s
                      AND permission = %s
                )
            """,
            params=[subject.type, subject.id, table.object_type, permission],
        )

    async def compile_rebac_filter_realtime(
        self,
        table: TableRef,
        subject: SubjectRef,
        permission: str,
    ) -> SQLFragment:
        """
        实时 ReBAC 过滤(适用于关系频繁变化的场景)
        通过 LookupResources 预查可访问的资源 ID 列表
        """
        accessible_ids = []
        async for resource in self._rebac_engine.lookup_resources(
            resource_type=table.object_type,
            permission=permission,
            subject=subject,
        ):
            accessible_ids.append(resource.id)

        if not accessible_ids:
            return SQLFragment(sql="1 = 0", params=[])

        if len(accessible_ids) > 1000:
            # ID 太多时使用临时表
            temp_table = await self._create_temp_table(accessible_ids)
            return SQLFragment(
                sql=f"{table.alias}.id IN (SELECT id FROM {temp_table})",
                params=[],
            )

        placeholders = ", ".join(["%s"] * len(accessible_ids))
        return SQLFragment(
            sql=f"{table.alias}.id IN ({placeholders})",
            params=accessible_ids,
        )

#4. 列级安全(Column-Level Security)

#4.1 列级策略类型

Python
class ColumnPolicy:
    """列级安全策略"""

    column: str
    object_type: str
    action: ColumnAction  # MASK / HIDE / TRANSFORM / ALLOW

class ColumnAction(Enum):
    ALLOW = "allow"        # 完全可见
    HIDE = "hide"          # 完全隐藏(从 SELECT 中移除)
    MASK = "mask"          # 脱敏显示
    TRANSFORM = "transform"  # 自定义转换

#4.2 列重写实现

Python
class ColumnLevelSecurityRewriter:
    """列级安全重写器"""

    async def rewrite_columns(
        self,
        query: ParsedQuery,
        subject: AuthenticatedUser,
        context: RequestContext,
    ) -> ParsedQuery:
        """重写查询的 SELECT 列表"""
        new_columns = []

        for col in query.select_columns:
            policy = await self._get_column_policy(
                col.table, col.name, subject, context
            )

            match policy.action:
                case ColumnAction.ALLOW:
                    new_columns.append(col)

                case ColumnAction.HIDE:
                    # 完全移除该列
                    continue

                case ColumnAction.MASK:
                    # 替换为脱敏表达式
                    mask_expr = self._get_mask_expression(
                        col, policy.mask_type, policy.mask_config
                    )
                    new_columns.append(
                        ColumnExpr(expression=mask_expr, alias=col.name)
                    )

                case ColumnAction.TRANSFORM:
                    # 自定义转换函数
                    transform_expr = policy.transform_expression
                    new_columns.append(
                        ColumnExpr(expression=transform_expr, alias=col.name)
                    )

        query.select_columns = new_columns
        return query

    def _get_mask_expression(
        self,
        col: ColumnRef,
        mask_type: str,
        config: dict,
    ) -> str:
        """生成脱敏 SQL 表达式"""
        full_name = f"{col.table_alias}.{col.name}"

        match mask_type:
            case "partial":
                # 部分显示:保留前N后M位
                prefix = config.get("prefix_length", 3)
                suffix = config.get("suffix_length", 4)
                mask_char = config.get("mask_char", "*")
                return (
                    f"CONCAT("
                    f"  LEFT({full_name}, {prefix}), "
                    f"  REPEAT('{mask_char}', GREATEST(LENGTH({full_name}) - {prefix + suffix}, 0)), "
                    f"  RIGHT({full_name}, {suffix})"
                    f")"
                )

            case "hash":
                # 哈希替换
                return f"MD5({full_name})"

            case "null":
                # 置 NULL
                return "NULL"

            case "constant":
                # 常量替换
                value = config.get("value", "***")
                return f"'{value}'"

            case "range":
                # 范围替换(数值型)
                bucket_size = config.get("bucket_size", 10)
                return f"FLOOR({full_name} / {bucket_size}) * {bucket_size}"

            case "date_truncate":
                # 日期截断
                precision = config.get("precision", "month")
                return f"DATE_TRUNC('{precision}', {full_name})"

#5. SQL 注入防护

#5.1 参数化查询强制

Python
class SafeSQLBuilder:
    """安全 SQL 构建器,强制参数化"""

    ALLOWED_COLUMN_PATTERN = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_.]*$')
    ALLOWED_TABLE_PATTERN = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_.]*$')

    def validate_identifier(self, identifier: str, kind: str) -> str:
        """验证 SQL 标识符(表名、列名)是否安全"""
        pattern = (
            self.ALLOWED_TABLE_PATTERN if kind == "table"
            else self.ALLOWED_COLUMN_PATTERN
        )
        if not pattern.match(identifier):
            raise UnsafeIdentifierError(
                f"Unsafe {kind} identifier: {identifier}"
            )
        return identifier

    def build_where_clause(self, fragments: list[SQLFragment]) -> SQLFragment:
        """安全地合并多个 WHERE 条件"""
        if not fragments:
            return SQLFragment(sql="1 = 1", params=[])

        # 所有值都通过参数化传递,永远不拼接用户输入
        sql_parts = []
        all_params = []
        for fragment in fragments:
            sql_parts.append(f"({fragment.sql})")
            all_params.extend(fragment.params)

        return SQLFragment(
            sql=" AND ".join(sql_parts),
            params=all_params,
        )

#5.2 查询审计

Python
class QueryAuditor:
    """查询审计器:记录所有重写操作"""

    async def audit_rewrite(
        self,
        original_query: str,
        rewritten_query: str,
        subject: AuthenticatedUser,
        applied_policies: list[str],
        context: RequestContext,
    ):
        """记录查询重写审计日志"""
        audit_event = QueryRewriteAuditEvent(
            timestamp=datetime.utcnow(),
            user_id=subject.user_id,
            original_query_hash=hashlib.sha256(original_query.encode()).hexdigest(),
            applied_policies=applied_policies,
            source_ip=context.source_ip,
            query_type=self._classify_query(original_query),
            tables_accessed=self._extract_tables(original_query),
            row_filters_applied=len(applied_policies),
            column_masks_applied=len([p for p in applied_policies if p.startswith("col-")]),
        )

        await self._audit_store.write(audit_event)

#6. 多数据源适配

#6.1 方言抽象层

coomia-dip 支持多种数据库,每种有不同的 SQL 方言:

Python
class SQLDialect(ABC):
    """SQL 方言抽象基类"""

    @abstractmethod
    def quote_identifier(self, name: str) -> str:
        """引用标识符"""

    @abstractmethod
    def param_placeholder(self, index: int) -> str:
        """参数占位符"""

    @abstractmethod
    def concat_function(self, *args: str) -> str:
        """字符串连接"""

    @abstractmethod
    def date_trunc(self, precision: str, column: str) -> str:
        """日期截断"""

    @abstractmethod
    def md5_hash(self, column: str) -> str:
        """MD5 哈希"""


class DorisDialect(SQLDialect):
    """Apache Doris SQL 方言"""

    def quote_identifier(self, name: str) -> str:
        return f"`{name}`"

    def param_placeholder(self, index: int) -> str:
        return "%s"

    def concat_function(self, *args: str) -> str:
        return f"CONCAT({', '.join(args)})"

    def date_trunc(self, precision: str, column: str) -> str:
        return f"DATE_TRUNC({column}, '{precision}')"

    def md5_hash(self, column: str) -> str:
        return f"MD5({column})"


class PostgreSQLDialect(SQLDialect):
    """PostgreSQL SQL 方言"""

    def quote_identifier(self, name: str) -> str:
        return f'"{name}"'

    def param_placeholder(self, index: int) -> str:
        return f"${index}"

    def concat_function(self, *args: str) -> str:
        return " || ".join(args)

    def date_trunc(self, precision: str, column: str) -> str:
        return f"DATE_TRUNC('{precision}', {column})"

    def md5_hash(self, column: str) -> str:
        return f"MD5({column}::text)"

#6.2 OQL 集成

coomia-dip 的 OQL(Ontology Query Language)原生支持策略重写:

Python
class OQLPolicyIntegration:
    """OQL 查询的策略集成"""

    async def rewrite_oql(
        self,
        oql: OQLQuery,
        subject: AuthenticatedUser,
        context: RequestContext,
    ) -> OQLQuery:
        """
        OQL 查询重写流程:
        1. OQL → AST
        2. AST 上注入策略条件
        3. AST → 目标 SQL(带策略条件)
        """
        ast = self._parser.parse(oql)

        # 在 AST 层面注入策略
        for object_type_ref in ast.referenced_types():
            policies = await self._policy_store.get_policies(
                object_type=object_type_ref.type_name,
                subject=subject,
            )
            for policy in policies:
                ast.inject_filter(object_type_ref, policy.to_ast_condition())

        return ast.to_oql()

#7. 物化权限视图

#7.1 权限预计算

对于读频繁的场景,预计算权限矩阵:

Python
class MaterializedPermissionView:
    """物化权限视图管理器"""

    async def refresh(self, object_type: str):
        """刷新物化权限视图"""
        await self._db.execute(f"""
            REFRESH MATERIALIZED VIEW CONCURRENTLY
            mv_permissions_{object_type}
        """)

    async def create_materialized_view(self, object_type: str):
        """创建物化权限视图"""
        await self._db.execute(f"""
            CREATE MATERIALIZED VIEW mv_permissions_{object_type} AS
            SELECT
                rt.resource_id,
                rt.subject_type,
                rt.subject_id,
                rt.relation AS permission,
                rt.created_at
            FROM relation_tuples rt
            WHERE rt.resource_type = '{object_type}'
              AND rt.deleted_at IS NULL
            UNION
            -- 展开继承关系
            SELECT
                child.resource_id,
                parent_rt.subject_type,
                parent_rt.subject_id,
                parent_rt.relation AS permission,
                parent_rt.created_at
            FROM relation_tuples child
            JOIN relation_tuples parent_rt
              ON child.subject_type = parent_rt.resource_type
             AND child.subject_id = parent_rt.resource_id
            WHERE child.resource_type = '{object_type}'
              AND child.deleted_at IS NULL
              AND parent_rt.deleted_at IS NULL
        """)

        # 创建索引
        await self._db.execute(f"""
            CREATE INDEX idx_mv_perm_{object_type}_subject
            ON mv_permissions_{object_type} (subject_type, subject_id);

            CREATE INDEX idx_mv_perm_{object_type}_resource
            ON mv_permissions_{object_type} (resource_id);
        """)

#7.2 增量更新

Python
class IncrementalPermissionUpdater:
    """增量权限更新器"""

    async def on_tuple_change(self, change: TupleChange):
        """当关系元组变更时,增量更新物化视图"""
        affected_types = await self._get_affected_types(change)

        for obj_type in affected_types:
            if change.operation == "TOUCH":
                await self._add_permission_entries(obj_type, change)
            elif change.operation == "DELETE":
                await self._remove_permission_entries(obj_type, change)

        # 失效相关缓存
        await self._invalidate_query_cache(affected_types)

    async def _get_affected_types(self, change: TupleChange) -> list[str]:
        """计算受影响的对象类型"""
        affected = {change.resource_type}

        # 查找以此对象为父对象的子类型
        child_types = await self._type_system.get_child_types(
            change.resource_type
        )
        affected.update(child_types)

        return list(affected)

#8. 性能优化

#8.1 策略编译缓存

Python
class PolicyCompilationCache:
    """策略编译结果缓存"""

    def __init__(self, max_size: int = 5000, ttl: int = 300):
        self._cache = TTLCache(maxsize=max_size, ttl=ttl)

    def cache_key(
        self,
        table: str,
        subject_fingerprint: str,
        context_fingerprint: str,
    ) -> str:
        """生成缓存键"""
        return f"pqr:{table}:{subject_fingerprint}:{context_fingerprint}"

    def get(self, key: str) -> Optional[list[SQLFragment]]:
        return self._cache.get(key)

    def set(self, key: str, fragments: list[SQLFragment]):
        self._cache[key] = fragments

#8.2 基准测试结果

Code
测试环境: 100 万行 orders 表, 5 个策略规则

| 方法                  | 延迟 (p50) | 延迟 (p99) | 内存峰值  |
|----------------------|-----------|-----------|----------|
| 应用层过滤             | 2400ms    | 5800ms    | 1.2 GB   |
| 查询重写(无缓存)      | 35ms      | 120ms     | 45 MB    |
| 查询重写(策略缓存)    | 28ms      | 95ms      | 45 MB    |
| 查询重写(物化视图)    | 15ms      | 55ms      | 30 MB    |
| 数据库原生 RLS          | 12ms      | 48ms      | 28 MB    |

结论: 查询重写相比应用层过滤,延迟降低 98%,内存降低 96%

#8.3 查询计划分析

Python
class QueryPlanAnalyzer:
    """查询计划分析器:确保重写后的查询使用索引"""

    async def analyze_rewritten_query(
        self,
        original: str,
        rewritten: str,
        params: list,
    ) -> QueryPlanReport:
        """分析重写后查询的执行计划"""
        plan = await self._db.execute(f"EXPLAIN ANALYZE {rewritten}", params)

        report = QueryPlanReport(
            uses_index=self._check_index_usage(plan),
            estimated_rows=self._extract_row_estimate(plan),
            actual_rows=self._extract_actual_rows(plan),
            seq_scan_tables=self._find_seq_scans(plan),
            total_cost=self._extract_total_cost(plan),
        )

        if report.seq_scan_tables:
            logger.warning(
                "Policy rewrite caused sequential scan on tables: %s. "
                "Consider adding indexes for policy filter columns.",
                report.seq_scan_tables,
            )

        return report

#9. 与 OQL 和 Ontology 的深度集成

#9.1 Ontology 感知的策略

Python
class OntologyAwarePolicyResolver:
    """Ontology 感知的策略解析器"""

    async def resolve_policies(
        self,
        object_type: str,
        subject: AuthenticatedUser,
    ) -> list[CompiledPolicy]:
        """
        根据 Ontology 的类型继承关系解析策略。
        子类型自动继承父类型的策略。
        """
        type_hierarchy = await self._ontology.get_type_hierarchy(object_type)

        all_policies = []
        for type_in_chain in type_hierarchy:
            policies = await self._policy_store.get_policies(type_in_chain)
            all_policies.extend(policies)

        # 子类型的策略可以 override 父类型的策略
        return self._merge_with_override(all_policies)

#9.2 关系属性过滤

Python
class RelationPropertyFilter:
    """基于关系属性的过滤"""

    async def compile_relation_filter(
        self,
        source_type: str,
        relation: str,
        target_type: str,
        subject: AuthenticatedUser,
    ) -> SQLFragment:
        """
        当查询涉及关系遍历时,在关系层面应用过滤。
        例如:查询 Project 的 Dataset 时,
        只返回用户有权限的 Dataset。
        """
        return SQLFragment(
            sql=f"""
                EXISTS (
                    SELECT 1 FROM relation_tuples rt
                    WHERE rt.resource_type = %s
                      AND rt.resource_id = {target_type}_table.id
                      AND rt.relation IN ('viewer', 'editor', 'owner')
                      AND rt.subject_type = 'user'
                      AND rt.subject_id = %s
                      AND rt.deleted_at IS NULL
                )
            """,
            params=[target_type, subject.user_id],
        )

#10. 生产运维

#10.1 策略变更管理

Python
class PolicyChangeManager:
    """策略变更管理器"""

    async def apply_policy_change(
        self,
        change: PolicyChange,
        approval: ApprovalRecord,
    ):
        """应用策略变更(需审批)"""
        # 1. 验证审批
        if not approval.is_approved:
            raise PolicyChangeNotApprovedError()

        # 2. 影响分析
        impact = await self._analyze_impact(change)
        logger.info(
            "Policy change impact: %d object types, ~%d rows affected",
            len(impact.affected_types),
            impact.estimated_affected_rows,
        )

        # 3. 灰度发布
        if impact.estimated_affected_rows > 100000:
            await self._gradual_rollout(change, impact)
        else:
            await self._immediate_apply(change)

        # 4. 刷新物化视图
        for obj_type in impact.affected_types:
            await self._materialized_view.refresh(obj_type)

        # 5. 清除编译缓存
        self._compilation_cache.clear()

#10.2 监控与告警

Python
POLICY_REWRITE_METRICS = {
    "pqr_compilation_total": Counter("策略编译总次数"),
    "pqr_compilation_latency": Histogram("策略编译延迟", buckets=[1, 5, 10, 50, 100]),
    "pqr_cache_hit_rate": Gauge("编译缓存命中率"),
    "pqr_rewrite_total": Counter("查询重写总次数"),
    "pqr_row_filter_count": Histogram("行过滤条件数量"),
    "pqr_column_mask_count": Histogram("列脱敏数量"),
    "pqr_query_slowdown": Histogram("重写导致的查询减速比"),
    "pqr_seq_scan_warnings": Counter("顺序扫描告警次数"),
    "pqr_policy_count": Gauge("活跃策略总数"),
}

#Key Takeaways

  1. 策略即查询重写将权限下推到数据层,避免应用层逐行过滤的性能灾难
  2. 策略编译器将声明式策略转为参数化 SQL,既安全又高效
  3. 行级安全通过 WHERE 子句注入实现,多策略间支持组内 OR、组间 AND 合并
  4. 列级安全通过 SELECT 列替换实现,支持脱敏、隐藏、转换等多种模式
  5. ReBAC 关系可通过物化视图预计算,将图遍历转为高效的 SQL JOIN
  6. 多数据源适配通过 SQL 方言抽象层实现,统一策略可编译为 Doris/PostgreSQL 等不同方言
  7. 基准测试证明查询重写相比应用层过滤,延迟降低 98%,内存降低 96%

#Next Article

下一篇 S6-06 动态数据脱敏:6 种模式 将深入探讨数据脱敏的 6 种实现模式,包括部分遮盖、哈希替换、范围化、日期截断、条件脱敏和格式保留加密。

#policy-as-query-rewrite #row-level-security #column-level-security #sql-injection-prevention #coomia-dip #platform-engineering