Skip to content

Commit 682e2fd

Browse files
authored
Add casbine-related interfaces (#107)
* Add casbin-related interfaces * format
1 parent c716d45 commit 682e2fd

File tree

8 files changed

+197
-38
lines changed

8 files changed

+197
-38
lines changed

backend/app/api/v1/casbin.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,93 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3-
from fastapi import APIRouter
3+
from typing import Annotated
4+
5+
from fastapi import APIRouter, Query
6+
7+
from backend.app.common.casbin_rbac import DependsRBAC
8+
from backend.app.common.jwt import DependsJwtAuth
9+
from backend.app.common.pagination import PageDepends, paging_data
10+
from backend.app.common.response.response_schema import response_base
11+
from backend.app.database.db_mysql import CurrentSession
12+
from backend.app.schemas.casbin_rule import (
13+
CreatePolicy,
14+
UpdatePolicy,
15+
DeletePolicy,
16+
CreateUserRole,
17+
DeleteUserRole,
18+
GetAllPolicy,
19+
)
20+
from backend.app.services.casbin_service import CasbinService
421

522
router = APIRouter()
623

7-
# TODO: 添加 casbin 相关接口
24+
25+
@router.get('', summary='(模糊条件)分页获取所有 casbin 规则', dependencies=[DependsJwtAuth, PageDepends])
26+
async def get_all_casbin(
27+
db: CurrentSession,
28+
ptype: Annotated[str | None, Query()] = None,
29+
sub: Annotated[str | None, Query()] = None,
30+
):
31+
casbin_select = await CasbinService.get_casbin_list(ptype=ptype, sub=sub)
32+
page_data = await paging_data(db, casbin_select, GetAllPolicy)
33+
return await response_base.success(data=page_data)
34+
35+
36+
@router.get('/policies', summary='获取所有 P 规则', dependencies=[DependsJwtAuth])
37+
async def get_all_policies():
38+
policies = await CasbinService.get_policy_list()
39+
return await response_base.success(data=policies)
40+
41+
42+
@router.post('/policy', summary='添加基于角色(主)/用户(次)的访问权限', dependencies=[DependsRBAC])
43+
async def create_policy(p: CreatePolicy):
44+
"""
45+
p 规则:
46+
47+
- 推荐添加基于角色的访问权限, 需配合添加 g 规则才能真正拥有访问权限,适合配置全局接口访问策略<br>
48+
**格式**: 角色 role + 访问路径 path + 访问方法 method
49+
50+
- 如果添加基于用户的访问权限, 不需配合添加 g 规则就能真正拥有权限,适合配置指定用户接口访问策略<br>
51+
**格式**: 用户 uuid + 访问路径 path + 访问方法 method
52+
"""
53+
data = await CasbinService.create_policy(p=p)
54+
return await response_base.success(data=data)
55+
56+
57+
@router.put('/policy', summary='更新基于角色(主)/用户(次)的访问权限', dependencies=[DependsRBAC])
58+
async def update_policy(old: UpdatePolicy, new: UpdatePolicy):
59+
data = await CasbinService.update_policy(old=old, new=new)
60+
return await response_base.success(data=data)
61+
62+
63+
@router.delete('/policy', summary='删除基于角色(主)/用户的访问权限', dependencies=[DependsRBAC])
64+
async def delete_policy(p: DeletePolicy):
65+
data = await CasbinService.delete_policy(p=p)
66+
return await response_base.success(data=data)
67+
68+
69+
@router.get('/groups', summary='获取所有 g 规则', dependencies=[DependsJwtAuth])
70+
async def get_all_groups():
71+
data = await CasbinService.get_group_list()
72+
return await response_base.success(data=data)
73+
74+
75+
@router.post('/group', summary='添加基于用户组的访问权限', dependencies=[DependsRBAC])
76+
async def create_group(g: CreateUserRole):
77+
"""
78+
g 规则 (**依赖 p 规则**):
79+
80+
- 如果在 p 规则中添加了基于角色的访问权限, 则还需要在 g 规则中添加基于用户组的访问权限, 才能真正拥有访问权限<br>
81+
**格式**: 用户 uuid + 角色 role
82+
83+
- 如果在p策略中添加了基于用户的访问权限, 则不添加相应的 g 规则能直接拥有访问权限<br>
84+
但是拥有的不是用户角色的所有权限, 而只是单一的对应的 p 规则所添加的访问权限
85+
"""
86+
data = await CasbinService.create_group(g=g)
87+
return await response_base.success(data=data)
88+
89+
90+
@router.delete('/group', summary='删除基于用户组的访问权限', dependencies=[DependsRBAC])
91+
async def delete_group(g: DeleteUserRole):
92+
data = await CasbinService.delete_group(g=g)
93+
return await response_base.success(data=data)

backend/app/api/v1/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,13 @@ async def get_all_route(request: Request):
5858
data = []
5959
for route in request.app.routes:
6060
if isinstance(route, APIRoute):
61-
data.append({'path': route.path, 'name': route.name, 'summary': route.summary, 'methods': route.methods})
61+
data.append(
62+
{
63+
'path': route.path,
64+
'name': route.name,
65+
'summary': route.summary,
66+
'methods': route.methods,
67+
'dependencies': route.dependencies,
68+
}
69+
)
6270
return await response_base.success(data={'route_list': data})

backend/app/common/casbin_rbac.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
class RBAC:
1616
@staticmethod
17-
async def get_casbin_enforcer() -> casbin.Enforcer:
17+
def enforcer() -> casbin.Enforcer:
1818
"""
1919
获取 casbin 执行器
2020
@@ -28,35 +28,34 @@ async def get_casbin_enforcer() -> casbin.Enforcer:
2828

2929
async def rbac_verify(self, request: Request, _: str = DependsJwtAuth) -> None:
3030
"""
31-
权限校验,超级用户跳过校验,默认拥有所有权限
31+
权限校验
3232
3333
:param request:
3434
:param _:
3535
:return:
3636
"""
37-
user_uuid = request.user.user_uuid
38-
user_roles = request.user.roles
39-
role_data_scope = [role.data_scope for role in user_roles]
4037
super_user = request.user.is_superuser
41-
path = request.url.path
42-
method = request.method
43-
4438
if super_user:
4539
return
4640

47-
for ce in settings.CASBIN_EXCLUDE:
48-
if ce['method'] == method and ce['path'] == path:
49-
return
41+
method = request.method
42+
path = request.url.path
43+
if (method, path) in settings.CASBIN_EXCLUDE:
44+
return
5045

51-
if 1 in set(role_data_scope):
46+
user_roles = request.user.roles
47+
data_scope = [role.data_scope for role in user_roles if role.data_scope == 1]
48+
if data_scope:
5249
return
5350

5451
# TODO: 通过 redis 做鉴权查询优化,减少数据库查询
55-
enforcer = await self.get_casbin_enforcer()
52+
user_uuid = request.user.user_uuid
53+
enforcer = self.enforcer()
5654
if not enforcer.enforce(user_uuid, path, method):
5755
raise AuthorizationError
5856

5957

60-
rbac = RBAC()
58+
RBAC = RBAC()
59+
RbacEnforcer = RBAC.enforcer()
6160
# RBAC 依赖注入
62-
DependsRBAC = Depends(rbac.rbac_verify)
61+
DependsRBAC = Depends(RBAC.rbac_verify)

backend/app/core/conf.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,12 @@ def validator_api_url(cls, values):
9898

9999
# Casbin
100100
CASBIN_RBAC_MODEL_NAME: str = 'rbac_model.conf'
101-
CASBIN_EXCLUDE: list[dict[str, str]] = [
102-
{'method': 'POST', 'path': '/v1/auth/swagger_login'},
103-
{'method': 'POST', 'path': '/v1/auth/login'},
104-
{'method': 'POST', 'path': '/v1/auth/register'},
105-
{'method': 'POST', 'path': '/v1/auth/password/reset'},
106-
]
101+
CASBIN_EXCLUDE: set[tuple[str, str]] = {
102+
('POST', '/v1/auth/swagger_login'),
103+
('POST', '/v1/auth/login'),
104+
('POST', '/v1/auth/register'),
105+
('POST', '/v1/auth/password/reset'),
106+
}
107107

108108
# Opera log
109109
OPERA_LOG_EXCLUDE: list[str] = [

backend/app/crud/crud_casbin.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3+
from sqlalchemy import Select, select, and_
4+
35
from backend.app.crud.base import CRUDBase
46
from backend.app.models import CasbinRule
57
from backend.app.schemas.casbin_rule import CreatePolicy, UpdatePolicy
68

79

810
class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicy, UpdatePolicy]):
9-
# TODO: 添加 casbin 相关数据库操作
10-
pass
11+
async def get_all_policy(self, ptype: str, sub: str) -> Select:
12+
se = select(self.model).order_by(self.model.id)
13+
where_list = []
14+
if ptype:
15+
where_list.append(self.model.ptype == ptype)
16+
if sub:
17+
where_list.append(self.model.v0.like(f'%{sub}%'))
18+
if where_list:
19+
se = se.where(and_(*where_list))
20+
return se
1121

1222

1323
CasbinDao: CRUDCasbin = CRUDCasbin(CasbinRule)

backend/app/schemas/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
class ApiBase(BaseModel):
1111
name: str
12-
method: str = Field(..., description='请求方法')
12+
method: str = Field(default=MethodType.GET, description='请求方法')
1313
path: str = Field(..., description='api路径')
1414
remark: str | None = None
1515

backend/app/schemas/casbin_rule.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55
from backend.app.common.enums import MethodType
66

77

8-
class RBACBase(BaseModel):
8+
class CreatePolicy(BaseModel):
99
sub: str = Field(..., description='用户uuid / 角色')
10-
11-
12-
class CreatePolicy(RBACBase):
13-
path: str = Field(..., description='api路径')
10+
path: str = Field(..., description='api 路径')
1411
method: str = Field(default=MethodType.GET, description='请求方法')
1512

1613
@validator('method')
@@ -32,15 +29,19 @@ class DeletePolicy(CreatePolicy):
3229

3330

3431
class CreateUserRole(BaseModel):
35-
uuid: str = Field(..., description='用户uuid')
32+
uuid: str = Field(..., description='用户 uuid')
3633
role: str = Field(..., description='角色')
3734

3835

36+
class DeleteUserRole(CreateUserRole):
37+
pass
38+
39+
3940
class GetAllPolicy(BaseModel):
4041
id: int
41-
ptype: str
42-
v0: str
43-
v1: str
42+
ptype: str = Field(..., description='规则类型, p 或 g')
43+
v0: str = Field(..., description='用户 uuid / 角色')
44+
v1: str = Field(..., description='api 路径 / 角色')
4445
v2: str | None = None
4546
v3: str | None = None
4647
v4: str | None = None
Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,62 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3+
from sqlalchemy import Select
4+
5+
from backend.app.common.casbin_rbac import RbacEnforcer
6+
from backend.app.common.exception import errors
7+
from backend.app.crud.crud_casbin import CasbinDao
8+
from backend.app.schemas.casbin_rule import CreatePolicy, UpdatePolicy, DeletePolicy, CreateUserRole, DeleteUserRole
39

410

511
class CasbinService:
6-
# TODO: 添加 casbin 相关服务
7-
pass
12+
@staticmethod
13+
async def get_casbin_list(*, ptype: str, sub: str) -> Select:
14+
return await CasbinDao.get_all_policy(ptype, sub)
15+
16+
@staticmethod
17+
async def get_policy_list():
18+
data = await RbacEnforcer.get_policy()
19+
return data
20+
21+
@staticmethod
22+
async def create_policy(*, p: CreatePolicy):
23+
data = await RbacEnforcer.add_policy(p.sub, p.path, p.method)
24+
if not data:
25+
raise errors.ForbiddenError(msg='权限已存在')
26+
return data
27+
28+
@staticmethod
29+
async def update_policy(*, old: UpdatePolicy, new: UpdatePolicy):
30+
_p = await RbacEnforcer.has_named_policy('p', old.sub, old.path, old.method)
31+
if not _p:
32+
raise errors.NotFoundError(msg='权限不存在')
33+
data = await RbacEnforcer.update_policy([old.sub, old.path, old.method], [new.sub, new.path, new.method])
34+
return data
35+
36+
@staticmethod
37+
async def delete_policy(*, p: DeletePolicy):
38+
_p = await RbacEnforcer.has_named_policy('p', p.sub, p.path, p.method)
39+
if not _p:
40+
raise errors.NotFoundError(msg='权限不存在')
41+
data = await RbacEnforcer.remove_policy(p.sub, p.path, p.method)
42+
return data
43+
44+
@staticmethod
45+
async def get_group_list():
46+
data = await RbacEnforcer.get_grouping_policy()
47+
return data
48+
49+
@staticmethod
50+
async def create_group(*, g: CreateUserRole):
51+
data = await RbacEnforcer.add_grouping_policy(g.uuid, g.role)
52+
if not data:
53+
raise errors.ForbiddenError(msg='权限已存在')
54+
return data
55+
56+
@staticmethod
57+
async def delete_group(*, g: DeleteUserRole):
58+
_g = await RbacEnforcer.has_named_grouping_policy('g', g.uuid, g.role)
59+
if not _g:
60+
raise errors.NotFoundError(msg='权限不存在')
61+
data = await RbacEnforcer.remove_grouping_policy(g.uuid, g.role)
62+
return data

0 commit comments

Comments
 (0)