Skip to content

Commit 9e817db

Browse files
authored
Add data permission rule value template variable (#1081)
* Add data permission rule value template variable * Improved models and columns
1 parent 0dfe74d commit 9e817db

File tree

5 files changed

+137
-61
lines changed

5 files changed

+137
-61
lines changed

backend/app/admin/api/v1/sys/data_rule.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
DeleteDataRuleParam,
88
GetDataRuleColumnDetail,
99
GetDataRuleDetail,
10+
GetDataRuleTemplateVariableDetail,
1011
UpdateDataRuleParam,
1112
)
1213
from backend.app.admin.service.data_rule_service import data_rule_service
@@ -34,6 +35,12 @@ async def get_data_rule_model_columns(
3435
return response_base.success(data=models)
3536

3637

38+
@router.get('/value-template-variables', summary='获取数据规则值可用模板变量', dependencies=[DependsJwtAuth])
39+
async def get_data_rule_value_template_variables() -> ResponseSchemaModel[list[GetDataRuleTemplateVariableDetail]]:
40+
variables = await data_rule_service.get_value_template_variables()
41+
return response_base.success(data=variables)
42+
43+
3744
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
3845
async def get_all_data_rules(db: CurrentSession) -> ResponseSchemaModel[list[GetDataRuleDetail]]:
3946
data = await data_rule_service.get_all(db=db)

backend/app/admin/schema/data_rule.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,10 @@ class GetDataRuleColumnDetail(SchemaBase):
4646

4747
key: str = Field(description='字段名')
4848
comment: str | None = Field(description='字段评论')
49+
50+
51+
class GetDataRuleTemplateVariableDetail(SchemaBase):
52+
"""数据规则可用模板变量详情"""
53+
54+
key: str = Field(description='变量标识')
55+
comment: str = Field(description='变量描述')

backend/app/admin/service/data_rule_service.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
CreateDataRuleParam,
1111
DeleteDataRuleParam,
1212
GetDataRuleColumnDetail,
13+
GetDataRuleTemplateVariableDetail,
1314
UpdateDataRuleParam,
1415
)
1516
from backend.app.admin.utils.cache import user_cache_manager
@@ -40,8 +41,16 @@ async def get(*, db: AsyncSession, pk: int) -> DataRule:
4041
@staticmethod
4142
async def get_models() -> list[str]:
4243
"""获取所有数据规则可用模型"""
43-
model_exclude = ['DataScope', 'DataRule', 'sys_role_data_scope', 'sys_data_scope_rule']
44-
return [m for m in list(get_data_permission_models().keys()) if m not in model_exclude]
44+
model_template_variables = [var['key'] for var in settings.DATA_PERMISSION_MODEL_TEMPLATE_VARIABLES]
45+
models = [
46+
m for m in list(get_data_permission_models().keys()) if m not in settings.DATA_PERMISSION_MODEL_EXCLUDE
47+
]
48+
return model_template_variables + models
49+
50+
@staticmethod
51+
async def get_value_template_variables() -> list[GetDataRuleTemplateVariableDetail]:
52+
"""获取所有数据规则值可用模板变量"""
53+
return [GetDataRuleTemplateVariableDetail(**var) for var in settings.DATA_PERMISSION_TEMPLATE_VARIABLES]
4554

4655
@staticmethod
4756
async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
@@ -51,6 +60,15 @@ async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
5160
:param model: 模型名称
5261
:return:
5362
"""
63+
column_template_variables = [
64+
GetDataRuleColumnDetail(key=var['key'], comment=var['comment'])
65+
for var in settings.DATA_PERMISSION_COLUMN_TEMPLATE_VARIABLES
66+
]
67+
68+
model_template_variable_keys = {var['key'] for var in settings.DATA_PERMISSION_MODEL_TEMPLATE_VARIABLES}
69+
if model in model_template_variable_keys:
70+
return column_template_variables
71+
5472
available_models = get_data_permission_models()
5573
if model not in available_models:
5674
raise errors.NotFoundError(msg='数据规则可用模型不存在')
@@ -62,7 +80,7 @@ async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
6280
for column in table.columns
6381
if column.key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
6482
]
65-
return model_columns
83+
return model_columns + column_template_variables
6684

6785
@staticmethod
6886
async def get_list(*, db: AsyncSession, name: str | None) -> dict[str, Any]:

backend/common/security/permission.py

Lines changed: 84 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import TYPE_CHECKING, Any
22

33
from fastapi import Request
44
from sqlalchemy import Alias, ColumnElement, Table, and_, or_
@@ -10,6 +10,10 @@
1010
from backend.common.exception import errors
1111
from backend.core.conf import settings
1212
from backend.utils.dynamic_import import get_all_models
13+
from backend.utils.timezone import timezone
14+
15+
if TYPE_CHECKING:
16+
from backend.app.admin.model import DataRule
1317

1418

1519
class RequestPermission:
@@ -69,80 +73,102 @@ def filter_data_permission( # noqa: C901
6973

7074
# 角色未启用数据权限过滤
7175
for role in request.user.roles:
72-
if not role.is_filter_scopes:
76+
if role.status and not role.is_filter_scopes:
7377
return or_(1 == 1)
7478

7579
# 获取数据规则
76-
data_rules = set()
80+
data_rules: set[DataRule] = set()
7781
for role in request.user.roles:
82+
if not role.status:
83+
continue
7884
for scope in role.scopes:
7985
if scope.status:
80-
data_rules.update(scope.rules)
86+
data_rules.update(rule for rule in scope.rules if rule is not None)
8187

8288
if not data_rules:
8389
return or_(1 == 1)
8490

85-
# 获取目标模型
86-
model_map = (
91+
# 目标模型
92+
target_model_map = (
8793
{getattr(model, '__name__', str(model)): model for model in models} if models else get_data_permission_models()
8894
)
8995

96+
# 字段模板变量映射
97+
column_template_resolvers = {
98+
var['key']: var['key'].strip('_') for var in settings.DATA_PERMISSION_COLUMN_TEMPLATE_VARIABLES
99+
}
100+
101+
# 模板变量解析映射
102+
template_variable_keys = {var['key'] for var in settings.DATA_PERMISSION_TEMPLATE_VARIABLES}
103+
template_resolvers = {
104+
'${user_id}': request.user.id,
105+
'${dept_id}': request.user.dept_id,
106+
'${now}': timezone.now,
107+
}
108+
90109
where_and_list = []
91110
where_or_list = []
92111

93112
for data_rule in data_rules:
94-
target_model = model_map.get(data_rule.model)
95-
if target_model is None:
96-
continue
97-
98-
table = target_model if isinstance(target_model, Table) else target_model.__table__
99-
rule_column = data_rule.column
100-
if rule_column not in table.columns.keys():
101-
continue
102-
if rule_column in settings.DATA_PERMISSION_COLUMN_EXCLUDE:
103-
continue
104-
105-
# 构建过滤条件
106-
column_obj = (
107-
getattr(target_model, rule_column) if not isinstance(target_model, Table) else table.columns[rule_column]
108-
)
109-
column_type = table.columns[rule_column].type.python_type
110-
111-
def cast_value(value: Any) -> Any:
112-
"""类型转换"""
113-
try:
114-
return column_type(value) if column_type is not str else value
115-
except (ValueError, TypeError):
116-
return value
117-
118-
condition = None
119-
match data_rule.expression:
120-
case RoleDataRuleExpressionType.eq:
121-
condition = column_obj == cast_value(data_rule.value)
122-
case RoleDataRuleExpressionType.ne:
123-
condition = column_obj != cast_value(data_rule.value)
124-
case RoleDataRuleExpressionType.gt:
125-
condition = column_obj > cast_value(data_rule.value)
126-
case RoleDataRuleExpressionType.ge:
127-
condition = column_obj >= cast_value(data_rule.value)
128-
case RoleDataRuleExpressionType.lt:
129-
condition = column_obj < cast_value(data_rule.value)
130-
case RoleDataRuleExpressionType.le:
131-
condition = column_obj <= cast_value(data_rule.value)
132-
case RoleDataRuleExpressionType.in_:
133-
values = [cast_value(v.strip()) for v in data_rule.value.split(',')]
134-
condition = column_obj.in_(values)
135-
case RoleDataRuleExpressionType.not_in:
136-
values = [cast_value(v.strip()) for v in data_rule.value.split(',')]
137-
condition = column_obj.not_in(values)
138-
139-
# 根据运算符添加到对应列表
140-
if condition is not None:
141-
match data_rule.operator:
142-
case RoleDataRuleOperatorType.AND:
143-
where_and_list.append(condition)
144-
case RoleDataRuleOperatorType.OR:
145-
where_or_list.append(condition)
113+
if data_rule.model == '__ALL__':
114+
target_models = list(target_model_map.values())
115+
else:
116+
target_model = target_model_map.get(data_rule.model)
117+
target_models = [target_model] if target_model is not None else []
118+
119+
for target_model in target_models:
120+
table = target_model if isinstance(target_model, Table) else target_model.__table__
121+
rule_column = column_template_resolvers.get(data_rule.column, data_rule.column)
122+
if rule_column not in table.columns.keys():
123+
continue
124+
if rule_column in settings.DATA_PERMISSION_COLUMN_EXCLUDE:
125+
continue
126+
127+
# 构建过滤条件
128+
column_obj = (
129+
getattr(target_model, rule_column)
130+
if not isinstance(target_model, Table)
131+
else table.columns[rule_column]
132+
)
133+
column_type = table.columns[rule_column].type.python_type
134+
135+
def cast_value(value: Any, _column_type: type = column_type) -> Any:
136+
"""类型转换"""
137+
try:
138+
if value in template_variable_keys:
139+
return _column_type(template_resolvers[value])
140+
return _column_type(value) if _column_type is not str else value
141+
except (ValueError, TypeError):
142+
return value
143+
144+
condition = None
145+
match data_rule.expression:
146+
case RoleDataRuleExpressionType.eq:
147+
condition = column_obj == cast_value(data_rule.value)
148+
case RoleDataRuleExpressionType.ne:
149+
condition = column_obj != cast_value(data_rule.value)
150+
case RoleDataRuleExpressionType.gt:
151+
condition = column_obj > cast_value(data_rule.value)
152+
case RoleDataRuleExpressionType.ge:
153+
condition = column_obj >= cast_value(data_rule.value)
154+
case RoleDataRuleExpressionType.lt:
155+
condition = column_obj < cast_value(data_rule.value)
156+
case RoleDataRuleExpressionType.le:
157+
condition = column_obj <= cast_value(data_rule.value)
158+
case RoleDataRuleExpressionType.in_:
159+
values = [cast_value(v.strip()) for v in data_rule.value.split(',')]
160+
condition = column_obj.in_(values)
161+
case RoleDataRuleExpressionType.not_in:
162+
values = [cast_value(v.strip()) for v in data_rule.value.split(',')]
163+
condition = column_obj.not_in(values)
164+
165+
# 根据运算符添加到对应列表
166+
if condition is not None:
167+
match data_rule.operator:
168+
case RoleDataRuleOperatorType.AND:
169+
where_and_list.append(condition)
170+
case RoleDataRuleOperatorType.OR:
171+
where_or_list.append(condition)
146172

147173
# 组合所有条件
148174
where_list = []

backend/core/conf.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,31 @@ def settings_customise_sources(
138138
COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
139139

140140
# 数据权限
141+
DATA_PERMISSION_MODEL_EXCLUDE: list[str] = [ # 排除允许进行数据过滤的 SQLA 模型
142+
'DataScope',
143+
'DataRule',
144+
'sys_role_data_scope',
145+
'sys_data_scope_rule',
146+
]
141147
DATA_PERMISSION_COLUMN_EXCLUDE: list[str] = [ # 排除允许进行数据过滤的 SQLA 模型列
142148
'id',
143149
'sort',
144150
'del_flag',
145151
'created_time',
146152
'updated_time',
147153
]
154+
DATA_PERMISSION_MODEL_TEMPLATE_VARIABLES: list[dict[str, str]] = [ # 数据规则模型可用模板变量
155+
{'key': '__ALL__', 'comment': '所有模型'},
156+
]
157+
DATA_PERMISSION_COLUMN_TEMPLATE_VARIABLES: list[dict[str, str]] = [ # 数据规则字段可用模板变量
158+
{'key': '__dept_id__', 'comment': '部门 ID'},
159+
{'key': '__created_by__', 'comment': '创建者'},
160+
]
161+
DATA_PERMISSION_TEMPLATE_VARIABLES: list[dict[str, str]] = [ # 数据规则值可用模板变量
162+
{'key': '${user_id}', 'comment': '当前登录用户 ID'},
163+
{'key': '${dept_id}', 'comment': '当前登录用户部门 ID'},
164+
{'key': '${now}', 'comment': '当前时间'},
165+
]
148166

149167
# Socket.IO
150168
WS_NO_AUTH_MARKER: str = 'internal'

0 commit comments

Comments
 (0)