diff --git a/backend/app/admin/api/v1/sys/data_rule.py b/backend/app/admin/api/v1/sys/data_rule.py index 5757b351..30dded33 100644 --- a/backend/app/admin/api/v1/sys/data_rule.py +++ b/backend/app/admin/api/v1/sys/data_rule.py @@ -7,6 +7,7 @@ from backend.app.admin.schema.data_rule import ( DeleteDataRuleParam, GetDataRuleColumnDetail, GetDataRuleDetail, + GetDataRuleTemplateVariableDetail, UpdateDataRuleParam, ) from backend.app.admin.service.data_rule_service import data_rule_service @@ -34,6 +35,12 @@ async def get_data_rule_model_columns( return response_base.success(data=models) +@router.get('/value-template-variables', summary='获取数据规则值可用模板变量', dependencies=[DependsJwtAuth]) +async def get_data_rule_value_template_variables() -> ResponseSchemaModel[list[GetDataRuleTemplateVariableDetail]]: + variables = await data_rule_service.get_value_template_variables() + return response_base.success(data=variables) + + @router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth]) async def get_all_data_rules(db: CurrentSession) -> ResponseSchemaModel[list[GetDataRuleDetail]]: data = await data_rule_service.get_all(db=db) diff --git a/backend/app/admin/schema/data_rule.py b/backend/app/admin/schema/data_rule.py index e991e174..da5092f4 100644 --- a/backend/app/admin/schema/data_rule.py +++ b/backend/app/admin/schema/data_rule.py @@ -46,3 +46,10 @@ class GetDataRuleColumnDetail(SchemaBase): key: str = Field(description='字段名') comment: str | None = Field(description='字段评论') + + +class GetDataRuleTemplateVariableDetail(SchemaBase): + """数据规则可用模板变量详情""" + + key: str = Field(description='变量标识') + comment: str = Field(description='变量描述') diff --git a/backend/app/admin/service/data_rule_service.py b/backend/app/admin/service/data_rule_service.py index ade9927f..447202b9 100644 --- a/backend/app/admin/service/data_rule_service.py +++ b/backend/app/admin/service/data_rule_service.py @@ -10,6 +10,7 @@ from backend.app.admin.schema.data_rule import ( CreateDataRuleParam, DeleteDataRuleParam, GetDataRuleColumnDetail, + GetDataRuleTemplateVariableDetail, UpdateDataRuleParam, ) from backend.app.admin.utils.cache import user_cache_manager @@ -40,8 +41,16 @@ class DataRuleService: @staticmethod async def get_models() -> list[str]: """获取所有数据规则可用模型""" - model_exclude = ['DataScope', 'DataRule', 'sys_role_data_scope', 'sys_data_scope_rule'] - return [m for m in list(get_data_permission_models().keys()) if m not in model_exclude] + model_template_variables = [var['key'] for var in settings.DATA_PERMISSION_MODEL_TEMPLATE_VARIABLES] + models = [ + m for m in list(get_data_permission_models().keys()) if m not in settings.DATA_PERMISSION_MODEL_EXCLUDE + ] + return model_template_variables + models + + @staticmethod + async def get_value_template_variables() -> list[GetDataRuleTemplateVariableDetail]: + """获取所有数据规则值可用模板变量""" + return [GetDataRuleTemplateVariableDetail(**var) for var in settings.DATA_PERMISSION_TEMPLATE_VARIABLES] @staticmethod async def get_columns(model: str) -> list[GetDataRuleColumnDetail]: @@ -51,6 +60,15 @@ class DataRuleService: :param model: 模型名称 :return: """ + column_template_variables = [ + GetDataRuleColumnDetail(key=var['key'], comment=var['comment']) + for var in settings.DATA_PERMISSION_COLUMN_TEMPLATE_VARIABLES + ] + + model_template_variable_keys = {var['key'] for var in settings.DATA_PERMISSION_MODEL_TEMPLATE_VARIABLES} + if model in model_template_variable_keys: + return column_template_variables + available_models = get_data_permission_models() if model not in available_models: raise errors.NotFoundError(msg='数据规则可用模型不存在') @@ -62,7 +80,7 @@ class DataRuleService: for column in table.columns if column.key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE ] - return model_columns + return model_columns + column_template_variables @staticmethod async def get_list(*, db: AsyncSession, name: str | None) -> dict[str, Any]: diff --git a/backend/common/security/permission.py b/backend/common/security/permission.py index 46168d8c..50ce7f1f 100644 --- a/backend/common/security/permission.py +++ b/backend/common/security/permission.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import TYPE_CHECKING, Any from fastapi import Request from sqlalchemy import Alias, ColumnElement, Table, and_, or_ @@ -10,6 +10,10 @@ from backend.common.enums import RoleDataRuleExpressionType, RoleDataRuleOperato from backend.common.exception import errors from backend.core.conf import settings from backend.utils.dynamic_import import get_all_models +from backend.utils.timezone import timezone + +if TYPE_CHECKING: + from backend.app.admin.model import DataRule class RequestPermission: @@ -69,80 +73,102 @@ def filter_data_permission( # noqa: C901 # 角色未启用数据权限过滤 for role in request.user.roles: - if not role.is_filter_scopes: + if role.status and not role.is_filter_scopes: return or_(1 == 1) # 获取数据规则 - data_rules = set() + data_rules: set[DataRule] = set() for role in request.user.roles: + if not role.status: + continue for scope in role.scopes: if scope.status: - data_rules.update(scope.rules) + data_rules.update(rule for rule in scope.rules if rule is not None) if not data_rules: return or_(1 == 1) - # 获取目标模型 - model_map = ( + # 目标模型 + target_model_map = ( {getattr(model, '__name__', str(model)): model for model in models} if models else get_data_permission_models() ) + # 字段模板变量映射 + column_template_resolvers = { + var['key']: var['key'].strip('_') for var in settings.DATA_PERMISSION_COLUMN_TEMPLATE_VARIABLES + } + + # 模板变量解析映射 + template_variable_keys = {var['key'] for var in settings.DATA_PERMISSION_TEMPLATE_VARIABLES} + template_resolvers = { + '${user_id}': request.user.id, + '${dept_id}': request.user.dept_id, + '${now}': timezone.now, + } + where_and_list = [] where_or_list = [] for data_rule in data_rules: - target_model = model_map.get(data_rule.model) - if target_model is None: - continue + if data_rule.model == '__ALL__': + target_models = list(target_model_map.values()) + else: + target_model = target_model_map.get(data_rule.model) + target_models = [target_model] if target_model is not None else [] - table = target_model if isinstance(target_model, Table) else target_model.__table__ - rule_column = data_rule.column - if rule_column not in table.columns.keys(): - continue - if rule_column in settings.DATA_PERMISSION_COLUMN_EXCLUDE: - continue + for target_model in target_models: + table = target_model if isinstance(target_model, Table) else target_model.__table__ + rule_column = column_template_resolvers.get(data_rule.column, data_rule.column) + if rule_column not in table.columns.keys(): + continue + if rule_column in settings.DATA_PERMISSION_COLUMN_EXCLUDE: + continue - # 构建过滤条件 - column_obj = ( - getattr(target_model, rule_column) if not isinstance(target_model, Table) else table.columns[rule_column] - ) - column_type = table.columns[rule_column].type.python_type + # 构建过滤条件 + column_obj = ( + getattr(target_model, rule_column) + if not isinstance(target_model, Table) + else table.columns[rule_column] + ) + column_type = table.columns[rule_column].type.python_type - def cast_value(value: Any) -> Any: - """类型转换""" - try: - return column_type(value) if column_type is not str else value - except (ValueError, TypeError): - return value + def cast_value(value: Any, _column_type: type = column_type) -> Any: + """类型转换""" + try: + if value in template_variable_keys: + return _column_type(template_resolvers[value]) + return _column_type(value) if _column_type is not str else value + except (ValueError, TypeError): + return value - condition = None - match data_rule.expression: - case RoleDataRuleExpressionType.eq: - condition = column_obj == cast_value(data_rule.value) - case RoleDataRuleExpressionType.ne: - condition = column_obj != cast_value(data_rule.value) - case RoleDataRuleExpressionType.gt: - condition = column_obj > cast_value(data_rule.value) - case RoleDataRuleExpressionType.ge: - condition = column_obj >= cast_value(data_rule.value) - case RoleDataRuleExpressionType.lt: - condition = column_obj < cast_value(data_rule.value) - case RoleDataRuleExpressionType.le: - condition = column_obj <= cast_value(data_rule.value) - case RoleDataRuleExpressionType.in_: - values = [cast_value(v.strip()) for v in data_rule.value.split(',')] - condition = column_obj.in_(values) - case RoleDataRuleExpressionType.not_in: - values = [cast_value(v.strip()) for v in data_rule.value.split(',')] - condition = column_obj.not_in(values) + condition = None + match data_rule.expression: + case RoleDataRuleExpressionType.eq: + condition = column_obj == cast_value(data_rule.value) + case RoleDataRuleExpressionType.ne: + condition = column_obj != cast_value(data_rule.value) + case RoleDataRuleExpressionType.gt: + condition = column_obj > cast_value(data_rule.value) + case RoleDataRuleExpressionType.ge: + condition = column_obj >= cast_value(data_rule.value) + case RoleDataRuleExpressionType.lt: + condition = column_obj < cast_value(data_rule.value) + case RoleDataRuleExpressionType.le: + condition = column_obj <= cast_value(data_rule.value) + case RoleDataRuleExpressionType.in_: + values = [cast_value(v.strip()) for v in data_rule.value.split(',')] + condition = column_obj.in_(values) + case RoleDataRuleExpressionType.not_in: + values = [cast_value(v.strip()) for v in data_rule.value.split(',')] + condition = column_obj.not_in(values) - # 根据运算符添加到对应列表 - if condition is not None: - match data_rule.operator: - case RoleDataRuleOperatorType.AND: - where_and_list.append(condition) - case RoleDataRuleOperatorType.OR: - where_or_list.append(condition) + # 根据运算符添加到对应列表 + if condition is not None: + match data_rule.operator: + case RoleDataRuleOperatorType.AND: + where_and_list.append(condition) + case RoleDataRuleOperatorType.OR: + where_or_list.append(condition) # 组合所有条件 where_list = [] diff --git a/backend/core/conf.py b/backend/core/conf.py index ceaa0d74..c5563eda 100644 --- a/backend/core/conf.py +++ b/backend/core/conf.py @@ -138,6 +138,12 @@ class Settings(BaseSettings): COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天 # 数据权限 + DATA_PERMISSION_MODEL_EXCLUDE: list[str] = [ # 排除允许进行数据过滤的 SQLA 模型 + 'DataScope', + 'DataRule', + 'sys_role_data_scope', + 'sys_data_scope_rule', + ] DATA_PERMISSION_COLUMN_EXCLUDE: list[str] = [ # 排除允许进行数据过滤的 SQLA 模型列 'id', 'sort', @@ -145,6 +151,18 @@ class Settings(BaseSettings): 'created_time', 'updated_time', ] + DATA_PERMISSION_MODEL_TEMPLATE_VARIABLES: list[dict[str, str]] = [ # 数据规则模型可用模板变量 + {'key': '__ALL__', 'comment': '所有模型'}, + ] + DATA_PERMISSION_COLUMN_TEMPLATE_VARIABLES: list[dict[str, str]] = [ # 数据规则字段可用模板变量 + {'key': '__dept_id__', 'comment': '部门 ID'}, + {'key': '__created_by__', 'comment': '创建者'}, + ] + DATA_PERMISSION_TEMPLATE_VARIABLES: list[dict[str, str]] = [ # 数据规则值可用模板变量 + {'key': '${user_id}', 'comment': '当前登录用户 ID'}, + {'key': '${dept_id}', 'comment': '当前登录用户部门 ID'}, + {'key': '${now}', 'comment': '当前时间'}, + ] # Socket.IO WS_NO_AUTH_MARKER: str = 'internal'