Skip to content

Commit be27d1b

Browse files
committed
Implement data filtering
1 parent 1f7f654 commit be27d1b

File tree

10 files changed

+150
-19
lines changed

10 files changed

+150
-19
lines changed

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@
1616
router = APIRouter()
1717

1818

19-
@router.get('/{pk}', summary='获取数据规则详情', dependencies=[DependsJwtAuth])
19+
@router.get('/{pk}', summary='获取数据权限规则详情', dependencies=[DependsJwtAuth])
2020
async def get_data_rule(pk: Annotated[int, Path(...)]) -> ResponseModel:
2121
data_rule = await data_rule_service.get(pk=pk)
2222
return response_base.success(data=data_rule)
2323

2424

25+
@router.get('/models', summary='获取支持过滤的数据库模型', dependencies=[DependsJwtAuth])
26+
async def get_data_rule_models() -> ResponseModel:
27+
models = await data_rule_service.get_models()
28+
return response_base.success(data=models)
29+
30+
2531
@router.get(
2632
'',
27-
summary='(模糊条件)分页获取所有数据规则',
33+
summary='(模糊条件)分页获取所有数据权限规则',
2834
dependencies=[
2935
DependsJwtAuth,
3036
DependsPagination,
@@ -38,7 +44,7 @@ async def get_pagination_data_rule(db: CurrentSession, name: Annotated[str | Non
3844

3945
@router.post(
4046
'',
41-
summary='创建数据规则',
47+
summary='创建数据权限规则',
4248
dependencies=[
4349
Depends(RequestPermission('data:rule:add')),
4450
DependsRBAC,
@@ -51,7 +57,7 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
5157

5258
@router.put(
5359
'/{pk}',
54-
summary='更新数据规则',
60+
summary='更新数据权限规则',
5561
dependencies=[
5662
Depends(RequestPermission('data:rule:edit')),
5763
DependsRBAC,
@@ -66,7 +72,7 @@ async def update_data_rule(pk: Annotated[int, Path(...)], obj: UpdateDataRulePar
6672

6773
@router.delete(
6874
'',
69-
summary='(批量)删除数据规则',
75+
summary='(批量)删除数据权限规则',
7076
dependencies=[
7177
Depends(RequestPermission('data:rule:del')),
7278
DependsRBAC,

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
router = APIRouter()
2121

2222

23-
@router.get('/{pk}', summary='获取数据规则类型详情', dependencies=[DependsJwtAuth])
23+
@router.get('/{pk}', summary='获取数据权限规则类型详情', dependencies=[DependsJwtAuth])
2424
async def get_data_rule_type(pk: Annotated[int, Path(...)]) -> ResponseModel:
2525
data_rule_type = await data_rule_type_service.get(pk=pk)
2626
return response_base.success(data=data_rule_type)
2727

2828

2929
@router.get(
3030
'',
31-
summary='(模糊条件)分页获取所有数据规则类型',
31+
summary='(模糊条件)分页获取所有数据权限规则类型',
3232
dependencies=[
3333
DependsJwtAuth,
3434
DependsPagination,
@@ -42,7 +42,7 @@ async def get_pagination_data_rule_type(db: CurrentSession) -> ResponseModel:
4242

4343
@router.post(
4444
'',
45-
summary='创建数据规则类型',
45+
summary='创建数据权限规则类型',
4646
dependencies=[
4747
Depends(RequestPermission('data:rule:type:add')),
4848
DependsRBAC,
@@ -55,7 +55,7 @@ async def create_data_rule_type(obj: CreateDataRuleTypeParam) -> ResponseModel:
5555

5656
@router.put(
5757
'/{pk}',
58-
summary='更新数据规则类型',
58+
summary='更新数据权限规则类型',
5959
dependencies=[
6060
Depends(RequestPermission('data:rule:type:edit')),
6161
DependsRBAC,
@@ -70,7 +70,7 @@ async def update_data_rule_type(pk: Annotated[int, Path(...)], obj: UpdateDataRu
7070

7171
@router.delete(
7272
'',
73-
summary='(批量)删除数据规则类型',
73+
summary='(批量)删除数据权限规则类型',
7474
dependencies=[
7575
Depends(RequestPermission('data:rule:type:del')),
7676
DependsRBAC,

backend/app/admin/crud/crud_role.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ async def get_with_relation(self, db, role_id: int) -> Role | None:
3333
:param role_id:
3434
:return:
3535
"""
36-
stmt = select(self.model).options(selectinload(self.model.menus), selectinload(self.model.rules)).where(self.model.id == role_id)
36+
stmt = (
37+
select(self.model)
38+
.options(selectinload(self.model.menus), selectinload(self.model.rules))
39+
.where(self.model.id == role_id)
40+
)
3741
role = await db.execute(stmt)
3842
return role.scalars().first()
3943

backend/app/admin/model/data_rule.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class DataRule(Base):
2020
expression: Mapped[int] = mapped_column(
2121
comment='表达式(0:>、1:>=、2:<、3:<=、4:==、5:!=、6:in、7:not_in)'
2222
)
23+
value: Mapped[str] = mapped_column(String(255), comment='规则值')
2324

2425
# 数据权限规则类型一对多
2526
type_id: Mapped[int] = mapped_column(

backend/app/admin/schema/data_rule.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class DataRuleSchemaBase(SchemaBase):
1515
column: str
1616
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.OR)
1717
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq)
18+
value: str
1819

1920

2021
class CreateDataRuleParam(DataRuleSchemaBase):

backend/app/admin/service/data_rule_service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ async def get(*, pk: int) -> DataRule:
2121
raise errors.NotFoundError(msg='数据规则不存在')
2222
return data_rule
2323

24+
@staticmethod
25+
async def get_models():
26+
return
27+
2428
@staticmethod
2529
async def get_select(*, name: str = None) -> Select:
2630
return await data_rule_dao.get_list(name=name)

backend/common/enums.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ class RoleDataRuleOperatorType(IntEnum):
4545
class RoleDataRuleExpressionType(IntEnum):
4646
"""数据权限规则表达式"""
4747

48-
gt = 0
49-
ge = 1
50-
lt = 2
51-
le = 3
52-
eq = 4
53-
ne = 5
48+
eq = 0
49+
ne = 1
50+
gt = 2
51+
ge = 3
52+
lt = 4
53+
le = 5
5454
in_ = 6
5555
not_in = 7
5656

backend/common/security/permission.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3+
from typing import TYPE_CHECKING
4+
35
from fastapi import Request
4-
from sqlalchemy import ColumnElement
6+
from sqlalchemy import ColumnElement, and_, or_
57

8+
from backend.common.enums import RoleDataRuleExpressionType, RoleDataRuleOperatorType
9+
from backend.common.exception import errors
610
from backend.common.exception.errors import ServerError
711
from backend.core.conf import settings
12+
from backend.utils.import_parse import dynamic_import
13+
14+
if TYPE_CHECKING:
15+
from backend.app.admin.schema.data_rule import GetDataRuleListDetails
816

917

1018
class RequestPermission:
@@ -27,4 +35,75 @@ async def __call__(self, request: Request):
2735
request.state.permission = self.value
2836

2937

30-
def filter_data_scope() -> ColumnElement[bool]: ...
38+
def filter_data_permission(request: Request) -> ColumnElement[bool]:
39+
"""
40+
过滤数据权限
41+
42+
使用场景:用户登录前台后,控制其能看到哪些数据
43+
44+
:param request:
45+
:return:
46+
"""
47+
user_data_rules: list[GetDataRuleListDetails] = request.user.roles.rules
48+
49+
# 超级管理员和无规则用户不做过滤
50+
if request.user.is_superuser or not user_data_rules:
51+
return or_(1 == 1)
52+
53+
where_and_list = []
54+
where_or_list = []
55+
allowed_models = frozenset(m.split('.')[-1] for m in settings.ALLOWED_MODELS)
56+
57+
for rule in user_data_rules:
58+
rule_model = rule.model
59+
if rule_model not in allowed_models:
60+
raise errors.NotFoundError(msg='数据模型不存在')
61+
try:
62+
model_ins = dynamic_import(rule_model)
63+
except Exception:
64+
raise errors.ServerError(msg='数据模型动态调用失败,请联系系统超级管理员')
65+
model_columns = model_ins.__table__.columns.keys()
66+
column = rule.column
67+
if column not in model_columns:
68+
raise errors.NotFoundError(msg='数据模型列不存在')
69+
70+
# 获取模型的列对象
71+
column_obj = getattr(model_ins, column)
72+
rule_expression = rule.expression
73+
74+
# 根据表达式类型构建条件
75+
condition = None
76+
if rule_expression == RoleDataRuleExpressionType.eq:
77+
condition = column_obj == rule.value
78+
elif rule_expression == RoleDataRuleExpressionType.ne:
79+
condition = column_obj != rule.value
80+
elif rule_expression == RoleDataRuleExpressionType.gt:
81+
condition = column_obj > rule.value
82+
elif rule_expression == RoleDataRuleExpressionType.ge:
83+
condition = column_obj >= rule.value
84+
elif rule_expression == RoleDataRuleExpressionType.lt:
85+
condition = column_obj < rule.value
86+
elif rule_expression == RoleDataRuleExpressionType.le:
87+
condition = column_obj <= rule.value
88+
elif rule_expression == RoleDataRuleExpressionType.in_:
89+
values = rule.value.split(',') if isinstance(rule.value, str) else rule.value
90+
condition = column_obj.in_(values)
91+
elif rule.expression == RoleDataRuleExpressionType.not_in:
92+
values = rule.value.split(',') if isinstance(rule.value, str) else rule.value
93+
condition = ~column_obj.in_(values)
94+
95+
if condition is not None:
96+
rule_operator = rule.operator
97+
if rule_operator == RoleDataRuleOperatorType.AND:
98+
where_and_list.append(condition)
99+
elif rule_operator == RoleDataRuleOperatorType.OR:
100+
where_or_list.append(condition)
101+
102+
# 组合条件
103+
where_list = []
104+
if where_and_list:
105+
where_list.append(and_(*where_and_list))
106+
if where_or_list:
107+
where_list.append(or_(*where_or_list))
108+
109+
return or_(*where_list) if where_list else or_(1 == 1)

backend/core/conf.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,13 @@ def validate_openapi_url(cls, values):
167167
'confirm_password',
168168
]
169169

170+
# Data Perms
171+
ALLOWED_MODELS: list[
172+
str
173+
] = [ # 允许进行数据过滤的 SQLA 模型,它必须以模块字符串的方式定义(它应该只用于前台数据,这里只是为了演示)
174+
'backend.app.admin.model.Api'
175+
]
176+
170177

171178
@lru_cache
172179
def get_settings() -> Settings:

backend/utils/import_parse.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
import importlib
4+
5+
from typing import Any
6+
7+
8+
def parse_module_str(module_path: str) -> tuple:
9+
"""
10+
Parse a module string into a Python module and class/function.
11+
12+
:param module_path:
13+
:return:
14+
"""
15+
module_name, class_or_func = module_path.rsplit('.', 1)
16+
return module_name, class_or_func
17+
18+
19+
def dynamic_import(module_path: str) -> Any:
20+
"""
21+
动态导入
22+
23+
:param module_path:
24+
:return:
25+
"""
26+
module_name, object_name = parse_module_str(module_path)
27+
module = importlib.import_module(module_name)
28+
class_or_func = getattr(module, object_name)
29+
return class_or_func

0 commit comments

Comments
 (0)