Reconstruct RBAC authentication logic (#264)

* Reconstruct RBAC authentication logic

* fix typo

* Migrate casbin sqla Adapter to redis

* Delete casbin model conf file

* Add permission dependencies

* Add request permission depends on execution condition

* Update openapi authorization method

* Add request permission identity

* Add request permission dependency description

* Migrate casbin redis adapter to sqla

* Update menu model and add function

* Fix menu permission identification

* Update user partial interface permissions

* Update menu table SQL

* Add role menu permission description to README

* fix README typo

* Simplify permission dependency injection

* Fix menu authorization store

* Fix interface permission dependency order

* Update role menu permission flag

* Update the background permission logic of the interface
This commit is contained in:
Wu Clan
2024-01-08 09:35:47 +08:00
committed by GitHub
parent f39d11eb9d
commit 5c7d6659cf
40 changed files with 654 additions and 220 deletions

View File

@ -53,6 +53,7 @@ See a preview of some of the screenshots
- [x] Global SQLAlchemy 2.0 syntax
- [x] Pydantic v1 and v2 (different branches)
- [x] Casbin RBAC access control model
- [x] Role menu RBAC access control model
- [x] Celery asynchronous tasks
- [x] JWT middleware whitelist authentication
- [x] Global customizable time zone time
@ -218,18 +219,13 @@ Execute unittests via pytest
- [Ruff](https://beta.ruff.rs/docs/)
- ...
## 互动
## Interactivity
We only have one current channel.
<table>
<tr>
<td style="text-align: center;"><a href="https://t.me/+ZlPhIFkPp7E4NGI1"> Jump </a></td>
</tr>
<tr>
<td> Telegram </td>
</tr>
</table>
| [Jump](https://t.me/+ZlPhIFkPp7E4NGI1) |
|----------------------------------------|
| Telegram |
## Sponsor us

View File

@ -47,6 +47,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
- [x] 全局 SQLAlchemy 2.0 语法
- [x] Pydantic v1 和 v2 (不同分支)
- [x] Casbin RBAC 访问控制模型
- [x] 角色菜单 RBAC 访问控制模型
- [x] Celery 异步任务
- [x] JWT 中间件白名单认证
- [x] 全局自定义时区时间
@ -216,14 +217,9 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
有且仅有当前一个频道,请注意辨别真伪
<table>
<tr>
<td style="text-align: center;"><a href="https://t.me/+ZlPhIFkPp7E4NGI1">直链跳转</a></td>
</tr>
<tr>
<td>Telegram科学上网</td>
</tr>
</table>
| [直链跳转](https://t.me/+ZlPhIFkPp7E4NGI1) |
|----------------------------------------|
| Telegram科学上网 |
## 赞助我们

View File

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import CurrentSession
@ -27,7 +28,14 @@ async def get_api(pk: int):
return await response_base.success(data=api)
@router.get('', summary='(模糊条件)分页获取所有接口', dependencies=[DependsJwtAuth, PageDepends])
@router.get(
'',
summary='(模糊条件)分页获取所有接口',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_api_list(
db: CurrentSession,
name: Annotated[str | None, Query()] = None,
@ -39,13 +47,27 @@ async def get_api_list(
return await response_base.success(data=page_data)
@router.post('', summary='创建接口', dependencies=[DependsRBAC])
@router.post(
'',
summary='创建接口',
dependencies=[
Depends(RequestPermission('sys:api:add')),
DependsRBAC,
],
)
async def create_api(obj: CreateApi):
await ApiService.create(obj=obj)
return await response_base.success()
@router.put('/{pk}', summary='更新接口', dependencies=[DependsRBAC])
@router.put(
'/{pk}',
summary='更新接口',
dependencies=[
Depends(RequestPermission('sys:api:edit')),
DependsRBAC,
],
)
async def update_api(pk: int, obj: UpdateApi):
count = await ApiService.update(pk=pk, obj=obj)
if count > 0:
@ -53,7 +75,14 @@ async def update_api(pk: int, obj: UpdateApi):
return await response_base.fail()
@router.delete('', summary='(批量)删除接口', dependencies=[DependsRBAC])
@router.delete(
'',
summary='(批量)删除接口',
dependencies=[
Depends(RequestPermission('sys:api:del')),
DependsRBAC,
],
)
async def delete_api(pk: Annotated[list[int], Query(...)]):
count = await ApiService.delete(pk=pk)
if count > 0:

View File

@ -16,7 +16,12 @@ from backend.app.services.auth_service import AuthService
router = APIRouter()
@router.post('/swagger_login', summary='swagger 表单登录', description='form 格式登录,仅用于 swagger 文档调试接口')
@router.post(
'/swagger_login',
summary='swagger 表单登录',
description='form 格式登录,用于 swagger 文档调试以及获取 JWT Auth',
deprecated=True,
)
async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -> GetSwaggerToken:
token, user = await AuthService().swagger_login(form_data=form_data)
return GetSwaggerToken(access_token=token, user=user) # type: ignore
@ -25,7 +30,7 @@ async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -
@router.post(
'/login',
summary='用户登录',
description='json 格式登录, 仅支持在第三方api工具调试接口, 例如: postman',
description='json 格式登录, 仅支持在第三方api工具调试, 例如: postman',
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
)
async def user_login(request: Request, obj: AuthLogin, background_tasks: BackgroundTasks):

View File

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Path, Query
from fastapi import APIRouter, Depends, Path, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import CurrentSession
@ -13,7 +14,6 @@ from backend.app.schemas.casbin_rule import (
CreatePolicy,
CreateUserRole,
DeleteAllPolicies,
DeleteAllUserRoles,
DeletePolicy,
DeleteUserRole,
GetAllPolicy,
@ -24,11 +24,18 @@ from backend.app.services.casbin_service import CasbinService
router = APIRouter()
@router.get('', summary='(模糊条件)分页获取所有权限规则', dependencies=[DependsJwtAuth, PageDepends])
@router.get(
'',
summary='(模糊条件)分页获取所有权限规则',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_all_casbin(
db: CurrentSession,
ptype: Annotated[str | None, Query()] = None,
sub: Annotated[str | None, Query()] = None,
ptype: Annotated[str | None, Query(description='规则类型, p / g')] = None,
sub: Annotated[str | None, Query(description='用户 uuid / 角色')] = None,
):
casbin_select = await CasbinService.get_casbin_list(ptype=ptype, sub=sub)
page_data = await paging_data(db, casbin_select, GetAllPolicy)
@ -47,7 +54,14 @@ async def get_role_policies(role: Annotated[str, Path(description='角色ID')]):
return await response_base.success(data=policies)
@router.post('/policy', summary='添加P权限规则', dependencies=[DependsRBAC])
@router.post(
'/policy',
summary='添加P权限规则',
dependencies=[
Depends(RequestPermission('casbin:p:add')),
DependsRBAC,
],
)
async def create_policy(p: CreatePolicy):
"""
p 规则:
@ -62,37 +76,79 @@ async def create_policy(p: CreatePolicy):
return await response_base.success(data=data)
@router.post('/policies', summary='添加多组P权限规则', dependencies=[DependsRBAC])
@router.post(
'/policies',
summary='添加多组P权限规则',
dependencies=[
Depends(RequestPermission('casbin:p:group:add')),
DependsRBAC,
],
)
async def create_policies(ps: list[CreatePolicy]):
data = await CasbinService.create_policies(ps=ps)
return await response_base.success(data=data)
@router.put('/policy', summary='更新P权限规则', dependencies=[DependsRBAC])
@router.put(
'/policy',
summary='更新P权限规则',
dependencies=[
Depends(RequestPermission('casbin:p:edit')),
DependsRBAC,
],
)
async def update_policy(old: UpdatePolicy, new: UpdatePolicy):
data = await CasbinService.update_policy(old=old, new=new)
return await response_base.success(data=data)
@router.put('/policies', summary='更新多组P权限规则', dependencies=[DependsRBAC])
@router.put(
'/policies',
summary='更新多组P权限规则',
dependencies=[
Depends(RequestPermission('casbin:p:group:edit')),
DependsRBAC,
],
)
async def update_policies(old: list[UpdatePolicy], new: list[UpdatePolicy]):
data = await CasbinService.update_policies(old=old, new=new)
return await response_base.success(data=data)
@router.delete('/policy', summary='删除P权限规则', dependencies=[DependsRBAC])
@router.delete(
'/policy',
summary='删除P权限规则',
dependencies=[
Depends(RequestPermission('casbin:p:del')),
DependsRBAC,
],
)
async def delete_policy(p: DeletePolicy):
data = await CasbinService.delete_policy(p=p)
return await response_base.success(data=data)
@router.delete('/policies', summary='删除多组P权限规则', dependencies=[DependsRBAC])
@router.delete(
'/policies',
summary='删除多组P权限规则',
dependencies=[
Depends(RequestPermission('casbin:p:group:del')),
DependsRBAC,
],
)
async def delete_policies(ps: list[DeletePolicy]):
data = await CasbinService.delete_policies(ps=ps)
return await response_base.success(data=data)
@router.delete('/policies/all', summary='删除所有P权限规则', dependencies=[DependsRBAC])
@router.delete(
'/policies/all',
summary='删除所有P权限规则',
dependencies=[
Depends(RequestPermission('casbin:p:empty')),
DependsRBAC,
],
)
async def delete_all_policies(sub: DeleteAllPolicies):
count = await CasbinService.delete_all_policies(sub=sub)
if count > 0:
@ -106,7 +162,14 @@ async def get_all_groups():
return await response_base.success(data=data)
@router.post('/group', summary='添加G权限规则', dependencies=[DependsRBAC])
@router.post(
'/group',
summary='添加G权限规则',
dependencies=[
Depends(RequestPermission('casbin:g:add')),
DependsRBAC,
],
)
async def create_group(g: CreateUserRole):
"""
g 规则 (**依赖 p 规则**):
@ -121,26 +184,54 @@ async def create_group(g: CreateUserRole):
return await response_base.success(data=data)
@router.post('/groups', summary='添加多组G权限规则', dependencies=[DependsRBAC])
@router.post(
'/groups',
summary='添加多组G权限规则',
dependencies=[
Depends(RequestPermission('casbin:g:group:add')),
DependsRBAC,
],
)
async def create_groups(gs: list[CreateUserRole]):
data = await CasbinService.create_groups(gs=gs)
return await response_base.success(data=data)
@router.delete('/group', summary='删除G权限规则', dependencies=[DependsRBAC])
@router.delete(
'/group',
summary='删除G权限规则',
dependencies=[
Depends(RequestPermission('casbin:g:del')),
DependsRBAC,
],
)
async def delete_group(g: DeleteUserRole):
data = await CasbinService.delete_group(g=g)
return await response_base.success(data=data)
@router.delete('/groups', summary='删除多组G权限规则', dependencies=[DependsRBAC])
@router.delete(
'/groups',
summary='删除多组G权限规则',
dependencies=[
Depends(RequestPermission('casbin:g:group:del')),
DependsRBAC,
],
)
async def delete_groups(gs: list[DeleteUserRole]):
data = await CasbinService.delete_groups(gs=gs)
return await response_base.success(data=data)
@router.delete('/groups/all', summary='删除所有G权限规则', dependencies=[DependsRBAC])
async def delete_all_groups(uuid: DeleteAllUserRoles):
@router.delete(
'/groups/all',
summary='删除所有G权限规则',
dependencies=[
Depends(RequestPermission('casbin:g:empty')),
DependsRBAC,
],
)
async def delete_all_groups(uuid: str):
count = await CasbinService.delete_all_groups(uuid=uuid)
if count > 0:
return await response_base.success()

View File

@ -2,9 +2,10 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.schemas.dept import CreateDept, GetAllDept, UpdateDept
@ -32,13 +33,27 @@ async def get_all_depts(
return await response_base.success(data=dept)
@router.post('', summary='创建部门', dependencies=[DependsRBAC])
@router.post(
'',
summary='创建部门',
dependencies=[
Depends(RequestPermission('sys:dept:add')),
DependsRBAC,
],
)
async def create_dept(obj: CreateDept):
await DeptService.create(obj=obj)
return await response_base.success()
@router.put('/{pk}', summary='更新部门', dependencies=[DependsRBAC])
@router.put(
'/{pk}',
summary='更新部门',
dependencies=[
Depends(RequestPermission('sys:dept:edit')),
DependsRBAC,
],
)
async def update_dept(pk: int, obj: UpdateDept):
count = await DeptService.update(pk=pk, obj=obj)
if count > 0:
@ -46,7 +61,14 @@ async def update_dept(pk: int, obj: UpdateDept):
return await response_base.fail()
@router.delete('{pk}', summary='删除部门', dependencies=[DependsRBAC])
@router.delete(
'{pk}',
summary='删除部门',
dependencies=[
Depends(RequestPermission('sys:dept:del')),
DependsRBAC,
],
)
async def delete_dept(pk: int):
count = await DeptService.delete(pk=pk)
if count > 0:

View File

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import CurrentSession
@ -23,7 +24,14 @@ async def get_dict_data(pk: int):
return await response_base.success(data=data)
@router.get('', summary='(模糊条件)分页获取所有字典', dependencies=[DependsJwtAuth, PageDepends])
@router.get(
'',
summary='(模糊条件)分页获取所有字典',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_all_dict_datas(
db: CurrentSession,
label: Annotated[str | None, Query()] = None,
@ -35,13 +43,27 @@ async def get_all_dict_datas(
return await response_base.success(data=page_data)
@router.post('', summary='创建字典', dependencies=[DependsRBAC])
@router.post(
'',
summary='创建字典',
dependencies=[
Depends(RequestPermission('sys:dict:data:add')),
DependsRBAC,
],
)
async def create_dict_data(obj: CreateDictData):
await DictDataService.create(obj=obj)
return await response_base.success()
@router.put('/{pk}', summary='更新字典', dependencies=[DependsRBAC])
@router.put(
'/{pk}',
summary='更新字典',
dependencies=[
Depends(RequestPermission('sys:dict:data:edit')),
DependsRBAC,
],
)
async def update_dict_data(pk: int, obj: UpdateDictData):
count = await DictDataService.update(pk=pk, obj=obj)
if count > 0:
@ -49,7 +71,14 @@ async def update_dict_data(pk: int, obj: UpdateDictData):
return await response_base.fail()
@router.delete('', summary='(批量)删除字典', dependencies=[DependsRBAC])
@router.delete(
'',
summary='(批量)删除字典',
dependencies=[
Depends(RequestPermission('sys:dict:data:del')),
DependsRBAC,
],
)
async def delete_dict_data(pk: Annotated[list[int], Query(...)]):
count = await DictDataService.delete(pk=pk)
if count > 0:

View File

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import CurrentSession
@ -15,7 +16,14 @@ from backend.app.services.dict_type_service import DictTypeService
router = APIRouter()
@router.get('', summary='(模糊条件)分页获取所有字典类型', dependencies=[DependsJwtAuth, PageDepends])
@router.get(
'',
summary='(模糊条件)分页获取所有字典类型',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_all_dict_types(
db: CurrentSession,
name: Annotated[str | None, Query()] = None,
@ -27,13 +35,27 @@ async def get_all_dict_types(
return await response_base.success(data=page_data)
@router.post('', summary='创建字典类型', dependencies=[DependsRBAC])
@router.post(
'',
summary='创建字典类型',
dependencies=[
Depends(RequestPermission('sys:dict:type:add')),
DependsRBAC,
],
)
async def create_dict_type(obj: CreateDictType):
await DictTypeService.create(obj=obj)
return await response_base.success()
@router.put('/{pk}', summary='更新字典类型', dependencies=[DependsRBAC])
@router.put(
'/{pk}',
summary='更新字典类型',
dependencies=[
Depends(RequestPermission('sys:dict:type:edit')),
DependsRBAC,
],
)
async def update_dict_type(pk: int, obj: UpdateDictType):
count = await DictTypeService.update(pk=pk, obj=obj)
if count > 0:
@ -41,7 +63,14 @@ async def update_dict_type(pk: int, obj: UpdateDictType):
return await response_base.fail()
@router.delete('', summary='(批量)删除字典类型', dependencies=[DependsRBAC])
@router.delete(
'',
summary='(批量)删除字典类型',
dependencies=[
Depends(RequestPermission('sys:dict:type:del')),
DependsRBAC,
],
)
async def delete_dict_type(pk: Annotated[list[int], Query(...)]):
count = await DictTypeService.delete(pk=pk)
if count > 0:

View File

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import CurrentSession
@ -15,7 +16,14 @@ from backend.app.services.login_log_service import LoginLogService
router = APIRouter()
@router.get('', summary='(模糊条件)分页获取登录日志', dependencies=[DependsJwtAuth, PageDepends])
@router.get(
'',
summary='(模糊条件)分页获取登录日志',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_all_login_logs(
db: CurrentSession,
username: Annotated[str | None, Query()] = None,
@ -27,7 +35,14 @@ async def get_all_login_logs(
return await response_base.success(data=page_data)
@router.delete('', summary='(批量)删除登录日志', dependencies=[DependsRBAC])
@router.delete(
'',
summary='(批量)删除登录日志',
dependencies=[
Depends(RequestPermission('log:login:del')),
DependsRBAC,
],
)
async def delete_login_log(pk: Annotated[list[int], Query(...)]):
count = await LoginLogService.delete(pk=pk)
if count > 0:
@ -35,7 +50,14 @@ async def delete_login_log(pk: Annotated[list[int], Query(...)]):
return await response_base.fail()
@router.delete('/all', summary='清空登录日志', dependencies=[DependsRBAC])
@router.delete(
'/all',
summary='清空登录日志',
dependencies=[
Depends(RequestPermission('log:login:empty')),
DependsRBAC,
],
)
async def delete_all_login_logs():
count = await LoginLogService.delete_all()
if count > 0:

View File

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import CurrentSession
@ -15,7 +16,14 @@ from backend.app.services.opera_log_service import OperaLogService
router = APIRouter()
@router.get('', summary='(模糊条件)分页获取操作日志', dependencies=[DependsJwtAuth, PageDepends])
@router.get(
'',
summary='(模糊条件)分页获取操作日志',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_all_opera_logs(
db: CurrentSession,
username: Annotated[str | None, Query()] = None,
@ -27,7 +35,14 @@ async def get_all_opera_logs(
return await response_base.success(data=page_data)
@router.delete('', summary='(批量)删除操作日志', dependencies=[DependsRBAC])
@router.delete(
'',
summary='(批量)删除操作日志',
dependencies=[
Depends(RequestPermission('log:opera:del')),
DependsRBAC,
],
)
async def delete_opera_log(pk: Annotated[list[int], Query(...)]):
count = await OperaLogService.delete(pk=pk)
if count > 0:
@ -35,7 +50,14 @@ async def delete_opera_log(pk: Annotated[list[int], Query(...)]):
return await response_base.fail()
@router.delete('/all', summary='清空操作日志', dependencies=[DependsRBAC])
@router.delete(
'/all',
summary='清空操作日志',
dependencies=[
Depends(RequestPermission('log:opera:empty')),
DependsRBAC,
],
)
async def delete_all_opera_logs():
count = await OperaLogService.delete_all()
if count > 0:

View File

@ -2,9 +2,10 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query, Request
from fastapi import APIRouter, Depends, Query, Request
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.schemas.menu import CreateMenu, GetAllMenu, UpdateMenu
@ -36,13 +37,27 @@ async def get_all_menus(
return await response_base.success(data=menu)
@router.post('', summary='创建菜单', dependencies=[DependsRBAC])
@router.post(
'',
summary='创建菜单',
dependencies=[
Depends(RequestPermission('sys:menu:add')),
DependsRBAC,
],
)
async def create_menu(obj: CreateMenu):
await MenuService.create(obj=obj)
return await response_base.success()
@router.put('/{pk}', summary='更新菜单', dependencies=[DependsRBAC])
@router.put(
'/{pk}',
summary='更新菜单',
dependencies=[
Depends(RequestPermission('sys:menu:edit')),
DependsRBAC,
],
)
async def update_menu(pk: int, obj: UpdateMenu):
count = await MenuService.update(pk=pk, obj=obj)
if count > 0:
@ -50,7 +65,14 @@ async def update_menu(pk: int, obj: UpdateMenu):
return await response_base.fail()
@router.delete('/{pk}', summary='删除菜单', dependencies=[DependsRBAC])
@router.delete(
'/{pk}',
summary='删除菜单',
dependencies=[
Depends(RequestPermission('sys:menu:del')),
DependsRBAC,
],
)
async def delete_menu(pk: int):
count = await MenuService.delete(pk=pk)
if count > 0:

View File

@ -1,15 +1,23 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Request
from fastapi import APIRouter, Depends, Request
from fastapi.routing import APIRoute
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
router = APIRouter()
@router.get('/routers', summary='获取所有路由', dependencies=[DependsRBAC])
@router.get(
'/routers',
summary='获取所有路由',
dependencies=[
Depends(RequestPermission('sys:route:list')),
DependsRBAC,
],
)
async def get_all_route(request: Request):
data = []
for route in request.app.routes:

View File

@ -1,15 +1,23 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.response.response_schema import response_base
from backend.app.utils.redis_info import redis_info
router = APIRouter()
@router.get('/redis', summary='redis 监控', dependencies=[DependsJwtAuth])
@router.get(
'/redis',
summary='redis 监控',
dependencies=[
Depends(RequestPermission('sys:monitor:redis')),
DependsJwtAuth,
],
)
async def get_redis_info():
data = {'info': await redis_info.get_info(), 'stats': await redis_info.get_stats()}
return await response_base.success(data=data)

View File

@ -1,16 +1,24 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from starlette.concurrency import run_in_threadpool
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.response.response_schema import response_base
from backend.app.utils.server_info import server_info
router = APIRouter()
@router.get('/server', summary='server 监控', dependencies=[DependsJwtAuth])
@router.get(
'/server',
summary='server 监控',
dependencies=[
Depends(RequestPermission('sys:monitor:server')),
DependsJwtAuth,
],
)
async def get_server_info():
"""IO密集型任务使用线程池尽量减少性能损耗"""
data = {

View File

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query
from fastapi import APIRouter, Depends, Query, Request
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import CurrentSession
@ -44,7 +45,14 @@ async def get_role(pk: int):
return await response_base.success(data=data)
@router.get('', summary='(模糊条件)分页获取所有角色', dependencies=[DependsJwtAuth, PageDepends])
@router.get(
'',
summary='(模糊条件)分页获取所有角色',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_all_role_list(
db: CurrentSession,
name: Annotated[str | None, Query()] = None,
@ -56,13 +64,27 @@ async def get_all_role_list(
return await response_base.success(data=page_data)
@router.post('', summary='创建角色', dependencies=[DependsRBAC])
@router.post(
'',
summary='创建角色',
dependencies=[
Depends(RequestPermission('sys:role:add')),
DependsRBAC,
],
)
async def create_role(obj: CreateRole):
await RoleService.create(obj=obj)
return await response_base.success()
@router.put('/{pk}', summary='更新角色', dependencies=[DependsRBAC])
@router.put(
'/{pk}',
summary='更新角色',
dependencies=[
Depends(RequestPermission('sys:role:edit')),
DependsRBAC,
],
)
async def update_role(pk: int, obj: UpdateRole):
count = await RoleService.update(pk=pk, obj=obj)
if count > 0:
@ -70,15 +92,29 @@ async def update_role(pk: int, obj: UpdateRole):
return await response_base.fail()
@router.put('/{pk}/menu', summary='更新角色菜单', dependencies=[DependsRBAC])
async def update_role_menu(pk: int, menu_ids: UpdateRoleMenu):
count = await RoleService.update_menus(pk=pk, menu_ids=menu_ids)
@router.put(
'/{pk}/menu',
summary='更新角色菜单',
dependencies=[
Depends(RequestPermission('sys:role:menu:edit')),
DependsRBAC,
],
)
async def update_role_menu(request: Request, pk: int, menu_ids: UpdateRoleMenu):
count = await RoleService.update_menus(request=request, pk=pk, menu_ids=menu_ids)
if count > 0:
return await response_base.success()
return await response_base.fail()
@router.delete('', summary='(批量)删除角色', dependencies=[DependsRBAC])
@router.delete(
'',
summary='(批量)删除角色',
dependencies=[
Depends(RequestPermission('sys:role:del')),
DependsRBAC,
],
)
async def delete_role(pk: Annotated[list[int], Query(...)]):
count = await RoleService.delete(pk=pk)
if count > 0:

View File

@ -2,9 +2,10 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Body, Path
from fastapi import APIRouter, Body, Depends, Path
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_code import CustomResponseCode
from backend.app.common.response.response_schema import response_base
@ -27,7 +28,14 @@ async def get_task_result(pk: str = Path(description='任务ID')):
return await response_base.success(data=task.result)
@router.post('/{module}', summary='执行任务', dependencies=[DependsRBAC])
@router.post(
'/{module}',
summary='执行任务',
dependencies=[
Depends(RequestPermission('sys:task:run')),
DependsRBAC,
],
)
async def run_task(
module: Annotated[str, Path(description='任务模块')],
args: Annotated[list | None, Body()] = None,

View File

@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Query, Request
from fastapi import APIRouter, Depends, Query, Request
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import PageDepends, paging_data
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import response_base
from backend.app.database.db_mysql import CurrentSession
@ -32,8 +33,8 @@ async def user_register(obj: RegisterUser):
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
async def add_user(obj: AddUser):
await UserService.add(obj=obj)
async def add_user(request: Request, obj: AddUser):
await UserService.add(request=request, obj=obj)
current_user = await UserService.get_userinfo(username=obj.username)
data = GetAllUserInfo(**await select_as_dict(current_user))
return await response_base.success(data=data)
@ -68,9 +69,16 @@ async def update_userinfo(request: Request, username: str, obj: UpdateUser):
return await response_base.fail()
@router.put('/{username}/role', summary='更新用户角色', dependencies=[DependsRBAC])
@router.put(
'/{username}/role',
summary='更新用户角色',
dependencies=[
Depends(RequestPermission('sys:user:role:edit')),
DependsRBAC,
],
)
async def update_user_role(request: Request, username: str, obj: UpdateUserRole):
await UserService.update_role(request=request, username=username, obj=obj)
await UserService.update_roles(request=request, username=username, obj=obj)
return await response_base.success()
@ -82,7 +90,14 @@ async def update_avatar(request: Request, username: str, avatar: Avatar):
return await response_base.fail()
@router.get('', summary='(模糊条件)分页获取所有用户', dependencies=[DependsJwtAuth, PageDepends])
@router.get(
'',
summary='(模糊条件)分页获取所有用户',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_all_users(
db: CurrentSession,
dept: Annotated[int | None, Query()] = None,
@ -131,10 +146,13 @@ async def multi_set(request: Request, pk: int):
path='/{username}',
summary='用户注销',
description='用户注销 != 用户登出,注销之后用户将从数据库删除',
dependencies=[DependsRBAC],
dependencies=[
Depends(RequestPermission('sys:user:del')),
DependsRBAC,
],
)
async def delete_user(request: Request, username: str):
count = await UserService.delete(request=request, username=username)
async def delete_user(username: str):
count = await UserService.delete(username=username)
if count > 0:
return await response_base.success()
return await response_base.fail()

View File

@ -50,6 +50,7 @@ class MethodType(StrEnum):
PUT = 'PUT'
DELETE = 'DELETE'
PATCH = 'PATCH'
OPTIONS = 'OPTIONS'
class LoginLogStatusType(IntEnum):

View File

@ -4,7 +4,7 @@ from datetime import datetime, timedelta
from asgiref.sync import sync_to_async
from fastapi import Depends, Request
from fastapi.security import OAuth2PasswordBearer
from fastapi.security import HTTPBearer, OAuth2PasswordBearer
from fastapi.security.utils import get_authorization_scheme_param
from jose import jwt
from passlib.context import CryptContext
@ -19,8 +19,12 @@ from backend.app.utils.timezone import timezone
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
# Deprecated, may be enabled when oauth2 is actually integrated
oauth2_schema = OAuth2PasswordBearer(tokenUrl=settings.TOKEN_URL_SWAGGER)
# JWT authorizes dependency injection
DependsJwtAuth = Depends(HTTPBearer())
@sync_to_async
def get_hash_password(password: str) -> str:
@ -209,8 +213,3 @@ def superuser_verify(request: Request) -> bool:
if not request.user.is_staff:
raise AuthorizationError(msg='此管理员已被禁止后台管理操作')
return is_superuser
# JWT authorizes dependency injection, which can be used if the interface only
# needs to provide a token instead of RBAC permission control
DependsJwtAuth = Depends(oauth2_schema)

View File

@ -84,4 +84,4 @@ async def paging_data(db: AsyncSession, select: Select, page_data_schema: Schema
# 分页依赖注入
PageDepends = Depends(pagination_ctx(_Page))
DependsPagination = Depends(pagination_ctx(_Page))

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import Request
from backend.app.common.exception.errors import ServerError
from backend.app.core.conf import settings
class RequestPermission:
"""
请求权限仅用于角色菜单RBAC
Tip:
使用此请求权限时,需要将 `Depends(RequestPermission('xxx'))` 在 `DependsRBAC` 之前设置,
因为 fastapi 当前版本的接口依赖注入按正序执行,意味着 RBAC 标识会在验证前被设置
"""
def __init__(self, value: str):
self.value = value
async def __call__(self, request: Request):
if settings.PERMISSION_MODE == 'role-menu':
if not isinstance(self.value, str):
raise ServerError
# 附加权限标识
request.state.permission = self.value

View File

@ -5,13 +5,13 @@ import casbin_async_sqlalchemy_adapter
from fastapi import Depends, Request
from backend.app.common.enums import StatusType
from backend.app.common.enums import StatusType, MethodType
from backend.app.common.exception.errors import AuthorizationError, TokenError
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.redis import redis_client
from backend.app.core.conf import settings
from backend.app.core.path_conf import RBAC_MODEL_CONF
from backend.app.database.db_mysql import async_engine
from backend.app.models.sys_casbin_rule import CasbinRule
from backend.app.models import CasbinRule
class RBAC:
@ -22,28 +22,46 @@ class RBAC:
:return:
"""
# 规则数据作为死数据直接在方法内定义
_CASBIN_RBAC_MODEL_CONF_TEXT = """
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
"""
adapter = casbin_async_sqlalchemy_adapter.Adapter(async_engine, db_class=CasbinRule)
enforcer = casbin.AsyncEnforcer(RBAC_MODEL_CONF, adapter)
model = casbin.AsyncEnforcer.new_model(text=_CASBIN_RBAC_MODEL_CONF_TEXT)
enforcer = casbin.AsyncEnforcer(model, adapter)
await enforcer.load_policy()
return enforcer
async def rbac_verify(self, request: Request, _: dict = DependsJwtAuth) -> None:
async def rbac_verify(self, request: Request, _token: str = DependsJwtAuth) -> None:
"""
RBAC 权限校验
:param request:
:param _:
:param _token:
:return:
"""
path = request.url.path
# 鉴权白名单
if path in settings.TOKEN_EXCLUDE:
return
# 强制校验 JWT 授权状态
# JWT 授权状态强制校验
if not request.auth.scopes:
raise TokenError
# 超级管理员免校验
super_user = request.user.is_superuser
if super_user:
if request.user.is_superuser:
return
# 检测角色数据权限范围
user_roles = request.user.roles
@ -51,44 +69,67 @@ class RBAC:
raise AuthorizationError(msg='用户未分配角色,授权失败')
if not any(len(role.menus) > 0 for role in user_roles):
raise AuthorizationError(msg='用户所属角色未分配菜单,授权失败')
method = request.method
if method != MethodType.GET or method != MethodType.OPTIONS:
if not request.user.is_staff:
raise AuthorizationError(msg='此用户已被禁止后台管理操作')
# 数据权限范围
data_scope = any(role.data_scope == 1 for role in user_roles)
if data_scope:
return
method = request.method
if settings.MENU_PERMISSION:
# 菜单权限校验
# TODO: 改用流行方案,自定义接口权限字段标识
path_auth = path.split(f'{settings.API_V1_STR}/')[-1].replace('/', ':') + f':{method}'
menu_perms = []
forbid_menu_perms = []
for role in user_roles:
if role.menus:
for menu in role.menus:
if menu.status == StatusType.enable:
menu_perms.append(menu.perms)
else:
forbid_menu_perms.append(menu.perms)
if path_auth in set(settings.MENU_EXCLUDE):
user_uuid = request.user.uuid
path_auth_perm = request.state.permission
if settings.PERMISSION_MODE == 'role-menu':
# 角色菜单权限校验
if path_auth_perm in set(settings.ROLE_MENU_EXCLUDE):
return
if path_auth in set([perm for perms_str in forbid_menu_perms for perm in perms_str.split(',')]):
user_menu_perms = await redis_client.get(f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:enable')
user_forbid_menu_perms = await redis_client.get(f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable')
if not user_menu_perms or not user_forbid_menu_perms:
user_menu_perms = []
user_forbid_menu_perms = []
for role in user_roles:
user_menus = role.menus
if user_menus:
for menu in user_menus:
perms = menu.perms
if perms:
if menu.status == StatusType.enable:
user_menu_perms.extend(perms.split(','))
else:
user_forbid_menu_perms.extend(perms.split(','))
await redis_client.set(
f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:enable', ','.join(user_menu_perms)
)
await redis_client.set(
f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable', ','.join(user_forbid_menu_perms)
)
if path_auth_perm in user_forbid_menu_perms:
raise AuthorizationError(msg='菜单已禁用,授权失败')
if path_auth not in set([perm for perms_str in menu_perms for perm in perms_str.split(',')]):
if path_auth_perm not in user_menu_perms:
raise AuthorizationError
else:
# casbin 权限校验
forbid_menu_path = []
user_forbid_menu_perms = await redis_client.get(
f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}:disable'
)
if not user_forbid_menu_perms:
user_forbid_menu_perms = []
for role in user_roles:
if role.menus:
for menu in role.menus:
user_menus = role.menus
if user_menus:
for menu in user_menus:
perms = menu.perms
if perms:
if menu.status == StatusType.disable:
forbid_menu_path.append(menu.path)
if path.split('/')[-1] in forbid_menu_path:
user_forbid_menu_perms.extend(perms.split(','))
await redis_client.set(
f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable', ','.join(user_forbid_menu_perms)
)
if path_auth_perm in user_forbid_menu_perms:
raise AuthorizationError(msg='菜单已禁用,授权失败')
if (method, path) in settings.CASBIN_EXCLUDE:
return
user_uuid = request.user.uuid
enforcer = await self.enforcer()
if not enforcer.enforce(user_uuid, path, method):
raise AuthorizationError

View File

@ -103,7 +103,7 @@ class Settings(BaseSettings):
TOKEN_URL_SWAGGER: str = f'{API_V1_STR}/auth/swagger_login'
TOKEN_REDIS_PREFIX: str = 'fba_token'
TOKEN_REFRESH_REDIS_PREFIX: str = 'fba_refresh_token'
TOKEN_EXCLUDE: list[str] = [ # 白名单
TOKEN_EXCLUDE: list[str] = [ # JWT / RBAC 白名单
f'{API_V1_STR}/auth/login',
]
@ -120,8 +120,11 @@ class Settings(BaseSettings):
MIDDLEWARE_GZIP: bool = True
MIDDLEWARE_ACCESS: bool = False
# Casbin
CASBIN_RBAC_MODEL_NAME: str = 'rbac_model.conf'
# RBAC Permission
PERMISSION_MODE: Literal['casbin', 'role-menu'] = 'casbin'
PERMISSION_REDIS_PREFIX: str = 'fba_permission'
# Casbin Auth
CASBIN_EXCLUDE: set[tuple[str, str]] = {
('POST', f'{API_V1_STR}/auth/swagger_login'),
('POST', f'{API_V1_STR}/auth/login'),
@ -130,14 +133,10 @@ class Settings(BaseSettings):
('GET', f'{API_V1_STR}/auth/captcha'),
}
# Menu
MENU_PERMISSION: bool = False # 危险行为,开启此功能, Casbin 鉴权将失效,并将使用角色菜单鉴权 (默认关闭)
MENU_EXCLUDE: list[str] = [
'auth:swagger_login:post',
'auth:login:post',
'auth:logout:post',
'auth:register:post',
'auth:captcha:get',
# Role Menu Auth
ROLE_MENU_EXCLUDE: list[str] = [
'sys:monitor:redis',
'sys:monitor:server',
]
# Opera log

View File

@ -4,8 +4,6 @@ import os
from pathlib import Path
from backend.app.core.conf import settings
# 获取项目根目录
# 或使用绝对路径指到backend目录为止例如windowsBasePath = D:\git_project\fastapi_mysql\backend
BasePath = Path(__file__).resolve().parent.parent.parent
@ -16,8 +14,5 @@ Versions = os.path.join(BasePath, 'app', 'alembic', 'versions')
# 日志文件路径
LogPath = os.path.join(BasePath, 'app', 'log')
# RBAC model.conf 文件路径
RBAC_MODEL_CONF = os.path.join(BasePath, 'app', 'core', settings.CASBIN_RBAC_MODEL_NAME)
# 离线 IP 数据库路径
IP2REGION_XDB = os.path.join(BasePath, 'app', 'static', 'ip2region.xdb')

View File

@ -1,14 +0,0 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*")

View File

@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.crud.base import CRUDBase
from backend.app.models import CasbinRule
from backend.app.schemas.casbin_rule import CreatePolicy, DeleteAllPolicies, DeleteAllUserRoles, UpdatePolicy
from backend.app.schemas.casbin_rule import CreatePolicy, DeleteAllPolicies, UpdatePolicy
class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicy, UpdatePolicy]):
@ -28,8 +28,8 @@ class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicy, UpdatePolicy]):
result = await db.execute(delete(self.model).where(or_(*where_list)))
return result.rowcount
async def delete_groups_by_uuid(self, db: AsyncSession, sub: DeleteAllUserRoles) -> int:
result = await db.execute(delete(self.model).where(self.model.v0 == sub.uuid))
async def delete_groups_by_uuid(self, db: AsyncSession, uuid: str) -> int:
result = await db.execute(delete(self.model).where(self.model.v0 == uuid))
return result.rowcount

View File

@ -15,7 +15,7 @@ class CRUDMenu(CRUDBase[Menu, CreateMenu, UpdateMenu]):
return await self.get_(db, pk=menu_id)
async def get_by_title(self, db, title: str) -> Menu | None:
result = await db.execute(select(self.model).where(self.model.title == title))
result = await db.execute(select(self.model).where(and_(self.model.title == title, self.model.menu_type != 2)))
return result.scalars().first()
async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
@ -43,7 +43,8 @@ class CRUDMenu(CRUDBase[Menu, CreateMenu, UpdateMenu]):
await self.create_(db, obj_in)
async def update(self, db, menu_id: int, obj_in: UpdateMenu) -> int:
return await self.update_(db, menu_id, obj_in)
count = await self.update_(db, menu_id, obj_in)
return count
async def delete(self, db, menu_id: int) -> int:
return await self.delete_(db, menu_id)

View File

@ -44,7 +44,7 @@ class CRUDUser(CRUDBase[User, RegisterUser, UpdateUser]):
async def add(self, db: AsyncSession, obj: AddUser) -> None:
salt = text_captcha(5)
obj.password = await jwt.get_hash_password(obj.password + salt)
dict_obj = obj.dict(exclude={'roles'})
dict_obj = obj.model_dump(exclude={'roles'})
dict_obj.update({'salt': salt})
new_user = self.model(**dict_obj)
role_list = []

View File

@ -8,13 +8,13 @@ from backend.app.models.base import MappedBase, id_key
class CasbinRule(MappedBase):
"""重写 casbin 中的 casbinRule model 类, 使用自定义 Base, 避免产生 alembic 迁移问题"""
"""重写 casbin 中的 CasbinRule model 类, 使用自定义 Base, 避免产生 alembic 迁移问题"""
__tablename__ = 'sys_casbin_rule'
id: Mapped[id_key]
ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p 或者 g')
v0: Mapped[str] = mapped_column(String(255), comment='角色 / 用户uuid')
ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p / g')
v0: Mapped[str] = mapped_column(String(255), comment='角色ID / 用户uuid')
v1: Mapped[str] = mapped_column(LONGTEXT, comment='api路径 / 角色名称')
v2: Mapped[str | None] = mapped_column(String(255), comment='请求方法')
v3: Mapped[str | None] = mapped_column(String(255))

View File

@ -16,7 +16,7 @@ class Menu(Base):
__tablename__ = 'sys_menu'
id: Mapped[id_key] = mapped_column(init=False)
title: Mapped[str] = mapped_column(String(50), unique=True, comment='菜单标题')
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
level: Mapped[int] = mapped_column(default=0, comment='菜单层级')
sort: Mapped[int] = mapped_column(default=0, comment='排序')

View File

@ -7,7 +7,7 @@ from backend.app.schemas.base import SchemaBase
class CreatePolicy(SchemaBase):
sub: str = Field(..., description='用户uuid / 角色')
sub: str = Field(..., description='用户uuid / 角色ID')
path: str = Field(..., description='api 路径')
method: MethodType = Field(default=MethodType.GET, description='请求方法')
@ -41,15 +41,11 @@ class DeleteUserRole(CreateUserRole):
pass
class DeleteAllUserRoles(SchemaBase):
uuid: str
class GetAllPolicy(SchemaBase):
model_config = ConfigDict(from_attributes=True)
id: int
ptype: str = Field(..., description='规则类型, p g')
ptype: str = Field(..., description='规则类型, p / g')
v0: str = Field(..., description='用户 uuid / 角色')
v1: str = Field(..., description='api 路径 / 角色')
v2: str | None = None

View File

@ -76,6 +76,9 @@ class GetAllUserInfo(GetUserInfoNoRelation):
class GetCurrentUserInfo(GetAllUserInfo):
model_config = ConfigDict(from_attributes=True)
dept: GetAllDept | str | None = None
roles: list[GetAllRole] | list[str] | None = None
@model_validator(mode='after')
def handel(self, values):
"""处理部门和角色"""

View File

@ -10,7 +10,6 @@ from backend.app.schemas.casbin_rule import (
CreatePolicy,
CreateUserRole,
DeleteAllPolicies,
DeleteAllUserRoles,
DeletePolicy,
DeleteUserRole,
UpdatePolicy,
@ -130,7 +129,7 @@ class CasbinService:
return data
@staticmethod
async def delete_all_groups(*, uuid: DeleteAllUserRoles) -> int:
async def delete_all_groups(*, uuid: str) -> int:
async with async_db_session.begin() as db:
count = await CasbinDao.delete_groups_by_uuid(db, uuid)
return count

View File

@ -5,6 +5,8 @@ from typing import Any
from fastapi import Request
from backend.app.common.exception import errors
from backend.app.common.redis import redis_client
from backend.app.core.conf import settings
from backend.app.crud.crud_menu import MenuDao
from backend.app.crud.crud_role import RoleDao
from backend.app.database.db_mysql import async_db_session
@ -79,6 +81,7 @@ class MenuService:
if not parent_menu:
raise errors.NotFoundError(msg='父级菜单不存在')
count = await MenuDao.update(db, pk, obj)
await redis_client.delete_prefix(settings.PERMISSION_REDIS_PREFIX)
return count
@staticmethod

View File

@ -2,9 +2,12 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from fastapi import Request
from sqlalchemy import Select
from backend.app.common.exception import errors
from backend.app.common.redis import redis_client
from backend.app.core.conf import settings
from backend.app.crud.crud_menu import MenuDao
from backend.app.crud.crud_role import RoleDao
from backend.app.database.db_mysql import async_db_session
@ -59,7 +62,7 @@ class RoleService:
return count
@staticmethod
async def update_menus(*, pk: int, menu_ids: UpdateRoleMenu) -> int:
async def update_menus(*, request: Request, pk: int, menu_ids: UpdateRoleMenu) -> int:
async with async_db_session.begin() as db:
role = await RoleDao.get(db, pk)
if not role:
@ -69,6 +72,7 @@ class RoleService:
if not menu:
raise errors.NotFoundError(msg='菜单不存在')
count = await RoleDao.update_menus(db, pk, menu_ids)
await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}')
return count
@staticmethod

View File

@ -34,8 +34,9 @@ class UserService:
await UserDao.create(db, obj)
@staticmethod
async def add(*, obj: AddUser) -> None:
async def add(*, request: Request, obj: AddUser) -> None:
async with async_db_session.begin() as db:
await superuser_verify(request)
username = await UserDao.get_by_username(db, obj.username)
if username:
raise errors.ForbiddenError(msg='此用户名已注册')
@ -85,7 +86,9 @@ class UserService:
@staticmethod
async def update(*, request: Request, username: str, obj: UpdateUser) -> int:
async with async_db_session.begin() as db:
await superuser_verify(request)
if not request.user.is_superuser:
if request.user.username != username:
raise errors.ForbiddenError(msg='你只能修改自己的信息')
input_user = await UserDao.get_with_relation(db, username=username)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')
@ -105,7 +108,7 @@ class UserService:
return count
@staticmethod
async def update_role(*, request: Request, username: str, obj: UpdateUserRole) -> None:
async def update_roles(*, request: Request, username: str, obj: UpdateUserRole) -> None:
async with async_db_session.begin() as db:
if not request.user.is_superuser:
if request.user.username != username:
@ -118,6 +121,7 @@ class UserService:
if not role:
raise errors.NotFoundError(msg='角色不存在')
await UserDao.update_role(db, input_user, obj)
await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}')
@staticmethod
async def update_avatar(*, request: Request, username: str, avatar: Avatar) -> int:
@ -196,9 +200,8 @@ class UserService:
return count
@staticmethod
async def delete(*, request: Request, username: str) -> int:
async def delete(*, username: str) -> int:
async with async_db_session.begin() as db:
await superuser_verify(request)
input_user = await UserDao.get_by_username(db, username)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')

View File

@ -114,8 +114,7 @@ CREATE TABLE sys_menu
created_time DATETIME NOT NULL COMMENT '创建时间',
updated_time DATETIME COMMENT '更新时间',
PRIMARY KEY (id),
FOREIGN KEY (parent_id) REFERENCES sys_menu (id) ON DELETE SET NULL,
UNIQUE (title)
FOREIGN KEY (parent_id) REFERENCES sys_menu (id) ON DELETE SET NULL
);
CREATE INDEX ix_sys_menu_id ON sys_menu (id);

View File

@ -2,7 +2,7 @@ INSERT INTO fba_test.sys_dept (id, name, level, sort, leader, phone, email, stat
VALUES (1, 'test', 0, 0, null, null, null, 1, 0, null, '2023-06-26 17:13:45', null);
INSERT INTO fba_test.sys_menu (id, title, name, level, sort, icon, path, menu_type, component, perms, status, `show`, cache, remark, parent_id, created_time, updated_time)
VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', '2023-07-27 19:14:52'),
VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', null),
(2, '仪表盘', 'dashboard', 0, 0, 'IconDashboard', 'dashboard', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:15:45', null),
(3, '工作台', 'Workplace', 0, 0, null, 'workplace', 1, '/dashboard/workplace/index.vue', null, 1, 1, 1, null, 2, '2023-07-27 19:17:59', null),
(4, 'arco官网', 'arcoWebsite', 0, 888, 'IconLink', 'https://arco.design', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:19:23', null),
@ -12,13 +12,28 @@ VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null
(8, '常见问题', 'faq', 0, 999, 'IconQuestion', 'https://arco.design/vue/docs/pro/faq', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:22:24', null),
(9, '系统管理', 'admin', 0, 6, 'IconSettings', 'admin', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:23:00', null),
(10, '部门管理', 'SysDept', 0, 0, null, 'sys-dept', 1, '/admin/dept/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:23:42', null),
(11, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null),
(12, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null),
(13, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null),
(14, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null),
(15, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null),
(16, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:03', null),
(17, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:29', null);
(11, '新增', '', 0, 0, null, null, 2, null, 'sys:dept:add', 1, 1, 1, null, 10, '2024-01-07 11:37:00', null),
(12, '编辑', '', 0, 0, null, null, 2, null, 'sys:dept:edit', 1, 1, 1, null, 10, '2024-01-07 11:37:29', null),
(13, '删除', '', 0, 0, null, null, 2, null, 'sys:dept:del', 1, 1, 1, null, 10, '2024-01-07 11:37:44', null),
(14, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null),
(15, '新增', '', 0, 0, null, null, 2, null, 'sys:api:add', 1, 1, 1, null, 14, '2024-01-07 11:57:09', null),
(16, '编辑', '', 0, 0, null, null, 2, null, 'sys:api:edit', 1, 1, 1, null, 14, '2024-01-07 11:57:44', null),
(17, '删除', '', 0, 0, null, null, 2, null, 'sys:api:del', 1, 1, 1, null, 14, '2024-01-07 11:57:56', null),
(18, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null),
(19, '编辑用户角色', '', 0, 0, null, null, 2, null, 'sys:user:role:edit', 1, 1, 1, null, 18, '2024-01-07 12:04:20', null),
(20, '注销', '', 0, 0, null, null, 2, null, 'sys:user:del', 1, 1, 1, '用户注销 != 用户登出,注销之后用户将从数据库删除', 18, '2024-01-07 02:28:09', null),
(21, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null),
(22, '新增', '', 0, 0, null, null, 2, null, 'sys:role:add', 1, 1, 1, null, 21, '2024-01-07 11:58:37', null),
(23, '编辑', '', 0, 0, null, null, 2, null, 'sys:role:edit', 1, 1, 1, null, 21, '2024-01-07 11:58:52', null),
(24, '删除', '', 0, 0, null, null, 2, null, 'sys:role:del', 1, 1, 1, null, 21, '2024-01-07 11:59:07', null),
(25, '编辑角色菜单', '', 0, 0, null, null, 2, null, 'sys:role:menu:edit', 1, 1, 1, null, 21, '2024-01-07 01:59:39', null),
(26, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null),
(27, '新增', '', 0, 0, null, null, 2, null, 'sys:menu:add', 1, 1, 1, null, 26, '2024-01-07 12:01:24', null),
(28, '编辑', '', 0, 0, null, null, 2, null, 'sys:menu:edit', 1, 1, 1, null, 26, '2024-01-07 12:01:34', null),
(29, '删除', '', 0, 0, null, null, 2, null, 'sys:menu:del', 1, 1, 1, null, 26, '2024-01-07 12:01:48', null),
(30, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null),
(31, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', 'sys:monitor:redis', 1, 1, 1, null, 30, '2023-07-27 19:28:03', null),
(32, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', 'sys:monitor:server', 1, 1, 1, null, 30, '2023-07-27 19:28:29', null);
INSERT INTO fba_test.sys_role (id, name, data_scope, status, remark, created_time, updated_time)
VALUES (1, 'test', 2, 1, null, '2023-06-26 17:13:45', null);

View File

@ -2,7 +2,7 @@ INSERT INTO fba.sys_dept (id, name, level, sort, leader, phone, email, status, d
VALUES (1, 'test', 0, 0, null, null, null, 1, 0, null, '2023-06-26 17:13:45', null);
INSERT INTO fba.sys_menu (id, title, name, level, sort, icon, path, menu_type, component, perms, status, `show`, cache, remark, parent_id, created_time, updated_time)
VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', '2023-07-27 19:14:52'),
VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', null),
(2, '仪表盘', 'dashboard', 0, 0, 'IconDashboard', 'dashboard', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:15:45', null),
(3, '工作台', 'Workplace', 0, 0, null, 'workplace', 1, '/dashboard/workplace/index.vue', null, 1, 1, 1, null, 2, '2023-07-27 19:17:59', null),
(4, 'arco官网', 'arcoWebsite', 0, 888, 'IconLink', 'https://arco.design', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:19:23', null),
@ -12,13 +12,28 @@ VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null
(8, '常见问题', 'faq', 0, 999, 'IconQuestion', 'https://arco.design/vue/docs/pro/faq', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:22:24', null),
(9, '系统管理', 'admin', 0, 6, 'IconSettings', 'admin', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:23:00', null),
(10, '部门管理', 'SysDept', 0, 0, null, 'sys-dept', 1, '/admin/dept/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:23:42', null),
(11, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null),
(12, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null),
(13, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null),
(14, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null),
(15, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null),
(16, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:03', null),
(17, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:29', null);
(11, '新增', '', 0, 0, null, null, 2, null, 'sys:dept:add', 1, 1, 1, null, 10, '2024-01-07 11:37:00', null),
(12, '编辑', '', 0, 0, null, null, 2, null, 'sys:dept:edit', 1, 1, 1, null, 10, '2024-01-07 11:37:29', null),
(13, '删除', '', 0, 0, null, null, 2, null, 'sys:dept:del', 1, 1, 1, null, 10, '2024-01-07 11:37:44', null),
(14, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null),
(15, '新增', '', 0, 0, null, null, 2, null, 'sys:api:add', 1, 1, 1, null, 14, '2024-01-07 11:57:09', null),
(16, '编辑', '', 0, 0, null, null, 2, null, 'sys:api:edit', 1, 1, 1, null, 14, '2024-01-07 11:57:44', null),
(17, '删除', '', 0, 0, null, null, 2, null, 'sys:api:del', 1, 1, 1, null, 14, '2024-01-07 11:57:56', null),
(18, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null),
(19, '编辑用户角色', '', 0, 0, null, null, 2, null, 'sys:user:role:edit', 1, 1, 1, null, 18, '2024-01-07 12:04:20', null),
(20, '注销', '', 0, 0, null, null, 2, null, 'sys:user:del', 1, 1, 1, '用户注销 != 用户登出,注销之后用户将从数据库删除', 18, '2024-01-07 02:28:09', null),
(21, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null),
(22, '新增', '', 0, 0, null, null, 2, null, 'sys:role:add', 1, 1, 1, null, 21, '2024-01-07 11:58:37', null),
(23, '编辑', '', 0, 0, null, null, 2, null, 'sys:role:edit', 1, 1, 1, null, 21, '2024-01-07 11:58:52', null),
(24, '删除', '', 0, 0, null, null, 2, null, 'sys:role:del', 1, 1, 1, null, 21, '2024-01-07 11:59:07', null),
(25, '编辑角色菜单', '', 0, 0, null, null, 2, null, 'sys:role:menu:edit', 1, 1, 1, null, 21, '2024-01-07 01:59:39', null),
(26, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null),
(27, '新增', '', 0, 0, null, null, 2, null, 'sys:menu:add', 1, 1, 1, null, 26, '2024-01-07 12:01:24', null),
(28, '编辑', '', 0, 0, null, null, 2, null, 'sys:menu:edit', 1, 1, 1, null, 26, '2024-01-07 12:01:34', null),
(29, '删除', '', 0, 0, null, null, 2, null, 'sys:menu:del', 1, 1, 1, null, 26, '2024-01-07 12:01:48', null),
(30, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null),
(31, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', 'sys:monitor:redis', 1, 1, 1, null, 30, '2023-07-27 19:28:03', null),
(32, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', 'sys:monitor:server', 1, 1, 1, null, 30, '2023-07-27 19:28:29', null);
INSERT INTO fba.sys_role (id, name, data_scope, status, remark, created_time, updated_time)
VALUES (1, 'test', 2, 1, null, '2023-06-26 17:13:45', null);

View File

@ -4,14 +4,14 @@ alembic==1.13.0
asgiref==3.7.2
asyncmy==0.2.9
bcrypt==4.0.1
casbin==1.33.0
casbin-async-sqlalchemy-adapter==1.3.0
casbin==1.34.0
casbin-async-sqlalchemy-adapter==1.4.0
celery==5.3.6
cryptography==41.0.7
email-validator==2.0.0
fast-captcha==0.2.1
fastapi==0.108.0
fastapi-limiter==0.1.5
fastapi-limiter==0.1.6
fastapi-pagination==0.12.13
gunicorn==21.2.0
httpx==0.25.2
@ -30,7 +30,7 @@ pytest-pretty==1.2.0
python-jose==3.3.0
python-multipart==0.0.6
pytz==2023.3
redis[hiredis]==4.5.5
redis[hiredis]==5.0.1
ruff==0.1.8
SQLAlchemy==2.0.23
supervisor==4.2.5