From 8adde717b5c43364f8e58b7b5f379a30813b4c6e Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 21 Oct 2024 18:33:55 +0800 Subject: [PATCH 1/7] Add department data operation permissions --- backend/common/security/rbac.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/common/security/rbac.py b/backend/common/security/rbac.py index a62ae5d6..0a8ec896 100644 --- a/backend/common/security/rbac.py +++ b/backend/common/security/rbac.py @@ -68,22 +68,21 @@ async def rbac_verify(self, request: Request, _token: str = DependsJwtAuth) -> N 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='此用户已被禁止后台管理操作') + raise AuthorizationError(msg='用户已被禁止后台管理操作,请联系系统管理员') # 数据权限范围 - data_scope = any(role.data_scope == 1 for role in user_roles) - if data_scope: + if any(role.data_scope == 1 for role in user_roles): return - user_uuid = request.user.uuid + # RBAC 鉴权 if settings.PERMISSION_MODE == 'role-menu': - # 角色菜单权限校验 path_auth_perm = getattr(request.state, 'permission', None) # 没有菜单权限标识不校验 if not path_auth_perm: return - if path_auth_perm in set(settings.RBAC_ROLE_MENU_EXCLUDE): + if path_auth_perm in settings.RBAC_ROLE_MENU_EXCLUDE: return allow_perms = [] for role in user_roles: @@ -93,7 +92,7 @@ async def rbac_verify(self, request: Request, _token: str = DependsJwtAuth) -> N if path_auth_perm not in allow_perms: raise AuthorizationError else: - # casbin 权限校验 + user_uuid = request.user.uuid if (method, path) in settings.RBAC_CASBIN_EXCLUDE: return enforcer = await self.enforcer() From e615f5c5afe26c7b1a9a571cc57da26e4eac1b15 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Tue, 22 Oct 2024 01:15:00 +0800 Subject: [PATCH 2/7] Update sys models --- .../app/admin/model/{sys_role_menu.py => m2m.py} | 8 ++++++++ backend/app/admin/model/sys_dept.py | 2 ++ backend/app/admin/model/sys_dict_data.py | 5 ++++- backend/app/admin/model/sys_dict_type.py | 1 + backend/app/admin/model/sys_menu.py | 8 ++++---- backend/app/admin/model/sys_role.py | 13 +++++-------- backend/app/admin/model/sys_user.py | 13 +++++++------ backend/app/admin/model/sys_user_role.py | 13 ------------- backend/app/admin/model/sys_user_social.py | 3 ++- 9 files changed, 33 insertions(+), 33 deletions(-) rename backend/app/admin/model/{sys_role_menu.py => m2m.py} (57%) delete mode 100644 backend/app/admin/model/sys_user_role.py diff --git a/backend/app/admin/model/sys_role_menu.py b/backend/app/admin/model/m2m.py similarity index 57% rename from backend/app/admin/model/sys_role_menu.py rename to backend/app/admin/model/m2m.py index e1f2cb7d..31040390 100644 --- a/backend/app/admin/model/sys_role_menu.py +++ b/backend/app/admin/model/m2m.py @@ -4,6 +4,14 @@ from backend.common.model import MappedBase +sys_user_role = Table( + 'sys_user_role', + MappedBase.metadata, + Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), + Column('user_id', Integer, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'), + Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'), +) + sys_role_menu = Table( 'sys_role_menu', MappedBase.metadata, diff --git a/backend/app/admin/model/sys_dept.py b/backend/app/admin/model/sys_dept.py index 2e1492e9..a85805d9 100644 --- a/backend/app/admin/model/sys_dept.py +++ b/backend/app/admin/model/sys_dept.py @@ -22,11 +22,13 @@ class Dept(Base): email: Mapped[str | None] = mapped_column(String(50), default=None, comment='邮箱') status: Mapped[int] = mapped_column(default=1, comment='部门状态(0停用 1正常)') del_flag: Mapped[bool] = mapped_column(default=False, comment='删除标志(0删除 1存在)') + # 父级部门一对多 parent_id: Mapped[int | None] = mapped_column( ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID' ) parent: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='children', remote_side=[id]) children: Mapped[list['Dept'] | None] = relationship(init=False, back_populates='parent') + # 部门用户一对多 users: Mapped[list['User']] = relationship(init=False, back_populates='dept') # noqa: F821 diff --git a/backend/app/admin/model/sys_dict_data.py b/backend/app/admin/model/sys_dict_data.py index 884bdc6b..4eac82b6 100644 --- a/backend/app/admin/model/sys_dict_data.py +++ b/backend/app/admin/model/sys_dict_data.py @@ -18,6 +18,9 @@ class DictData(Base): sort: Mapped[int] = mapped_column(default=0, comment='排序') status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)') remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注') + # 字典类型一对多 - type_id: Mapped[int] = mapped_column(ForeignKey('sys_dict_type.id'), default=0, comment='字典类型关联ID') + type_id: Mapped[int] = mapped_column( + ForeignKey('sys_dict_type.id', ondelete='CASCADE'), default=0, comment='字典类型关联ID' + ) type: Mapped['DictType'] = relationship(init=False, back_populates='datas') # noqa: F821 diff --git a/backend/app/admin/model/sys_dict_type.py b/backend/app/admin/model/sys_dict_type.py index 3190162e..c5676b4a 100644 --- a/backend/app/admin/model/sys_dict_type.py +++ b/backend/app/admin/model/sys_dict_type.py @@ -17,5 +17,6 @@ class DictType(Base): code: Mapped[str] = mapped_column(String(32), unique=True, comment='字典类型编码') status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)') remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注') + # 字典类型一对多 datas: Mapped[list['DictData']] = relationship(init=False, back_populates='type') # noqa: F821 diff --git a/backend/app/admin/model/sys_menu.py b/backend/app/admin/model/sys_menu.py index f3233b2c..8b75d29e 100644 --- a/backend/app/admin/model/sys_menu.py +++ b/backend/app/admin/model/sys_menu.py @@ -6,7 +6,7 @@ from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.admin.model.sys_role_menu import sys_role_menu +from backend.app.admin.model.m2m import sys_role_menu from backend.common.model import Base, id_key @@ -29,13 +29,13 @@ class Menu(Base): show: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)') cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)') remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注') + # 父级菜单一对多 parent_id: Mapped[int | None] = mapped_column( ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID' ) parent: Mapped[Union['Menu', None]] = relationship(init=False, back_populates='children', remote_side=[id]) children: Mapped[list['Menu'] | None] = relationship(init=False, back_populates='parent') + # 菜单角色多对多 - roles: Mapped[list['Role']] = relationship( # noqa: F821 - init=False, secondary=sys_role_menu, back_populates='menus' - ) + roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_menu, back_populates='menus') # noqa: F821 diff --git a/backend/app/admin/model/sys_role.py b/backend/app/admin/model/sys_role.py index acf46c01..415329f1 100644 --- a/backend/app/admin/model/sys_role.py +++ b/backend/app/admin/model/sys_role.py @@ -4,8 +4,7 @@ from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.admin.model.sys_role_menu import sys_role_menu -from backend.app.admin.model.sys_user_role import sys_user_role +from backend.app.admin.model.m2m import sys_role_menu, sys_user_role from backend.common.model import Base, id_key @@ -19,11 +18,9 @@ class Role(Base): data_scope: Mapped[int | None] = mapped_column(default=2, comment='权限范围(1:全部数据权限 2:自定义数据权限)') status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)') remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注') + # 角色用户多对多 - users: Mapped[list['User']] = relationship( # noqa: F821 - init=False, secondary=sys_user_role, back_populates='roles' - ) + users: Mapped[list['User']] = relationship(init=False, secondary=sys_user_role, back_populates='roles') # noqa: F821 + # 角色菜单多对多 - menus: Mapped[list['Menu']] = relationship( # noqa: F821 - init=False, secondary=sys_role_menu, back_populates='roles' - ) + menus: Mapped[list['Menu']] = relationship(init=False, secondary=sys_role_menu, back_populates='roles') # noqa: F821 diff --git a/backend/app/admin/model/sys_user.py b/backend/app/admin/model/sys_user.py index dab80193..5a11d429 100644 --- a/backend/app/admin/model/sys_user.py +++ b/backend/app/admin/model/sys_user.py @@ -6,7 +6,7 @@ from sqlalchemy import ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.admin.model.sys_user_role import sys_user_role +from backend.app.admin.model.m2m import sys_user_role from backend.common.model import Base, id_key from backend.database.db_mysql import uuid4_str from backend.utils.timezone import timezone @@ -32,14 +32,15 @@ class User(Base): phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号') join_time: Mapped[datetime] = mapped_column(init=False, default_factory=timezone.now, comment='注册时间') last_login_time: Mapped[datetime | None] = mapped_column(init=False, onupdate=timezone.now, comment='上次登录') + # 部门用户一对多 dept_id: Mapped[int | None] = mapped_column( ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID' ) dept: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='users') # noqa: F821 - # 用户角色多对多 - roles: Mapped[list['Role']] = relationship( # noqa: F821 - init=False, secondary=sys_user_role, back_populates='users' - ) - # 用户 OAuth2 一对多 + + # 用户社交信息一对多 socials: Mapped[list['UserSocial']] = relationship(init=False, back_populates='user') # noqa: F821 + + # 用户角色多对多 + roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_user_role, back_populates='users') # noqa: F821 diff --git a/backend/app/admin/model/sys_user_role.py b/backend/app/admin/model/sys_user_role.py deleted file mode 100644 index 1870360e..00000000 --- a/backend/app/admin/model/sys_user_role.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from sqlalchemy import INT, Column, ForeignKey, Integer, Table - -from backend.common.model import MappedBase - -sys_user_role = Table( - 'sys_user_role', - MappedBase.metadata, - Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), - Column('user_id', Integer, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'), - Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'), -) diff --git a/backend/app/admin/model/sys_user_social.py b/backend/app/admin/model/sys_user_social.py index 42c8833a..50439aa6 100644 --- a/backend/app/admin/model/sys_user_social.py +++ b/backend/app/admin/model/sys_user_social.py @@ -20,7 +20,8 @@ class UserSocial(Base): union_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 union id') scope: Mapped[str | None] = mapped_column(String(120), default=None, comment='第三方用户授予的权限') code: Mapped[str | None] = mapped_column(String(50), default=None, comment='用户的授权 code') - # 用户 OAuth2 一对多 + + # 用户社交信息一对多 user_id: Mapped[int | None] = mapped_column( ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID' ) From 884ea605d959e62bd85eb8222a542e9424fdf11c Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 24 Oct 2024 21:05:20 +0800 Subject: [PATCH 3/7] Add role department many-to-many relationship --- backend/app/admin/model/m2m.py | 8 ++++++++ backend/app/admin/model/sys_role.py | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/app/admin/model/m2m.py b/backend/app/admin/model/m2m.py index 31040390..6984e70e 100644 --- a/backend/app/admin/model/m2m.py +++ b/backend/app/admin/model/m2m.py @@ -19,3 +19,11 @@ Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'), Column('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'), ) + +sys_role_dept = Table( + 'sys_role_dept', + MappedBase.metadata, + Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), + Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'), + Column('dept_id', Integer, ForeignKey('sys_dept.id', ondelete='CASCADE'), primary_key=True, comment='部门ID'), +) diff --git a/backend/app/admin/model/sys_role.py b/backend/app/admin/model/sys_role.py index 415329f1..73cbf8dd 100644 --- a/backend/app/admin/model/sys_role.py +++ b/backend/app/admin/model/sys_role.py @@ -15,7 +15,9 @@ class Role(Base): id: Mapped[id_key] = mapped_column(init=False) name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称') - data_scope: Mapped[int | None] = mapped_column(default=2, comment='权限范围(1:全部数据权限 2:自定义数据权限)') + data_scope: Mapped[int | None] = mapped_column( + default=2, comment='权限范围(0: 全部数据,1: 本人数据,2: 所在部门数据,3: 所在部门及以下数据,4: 自定义数据)' + ) status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)') remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注') From addd0a11449cb24171d5e3b91d73b5ee36b50627 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 25 Oct 2024 17:50:14 +0800 Subject: [PATCH 4/7] Format RBAC code --- backend/app/generator/api/v1/gen.py | 2 +- backend/common/security/rbac.py | 36 +++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/backend/app/generator/api/v1/gen.py b/backend/app/generator/api/v1/gen.py index d3830bea..5713c29d 100644 --- a/backend/app/generator/api/v1/gen.py +++ b/backend/app/generator/api/v1/gen.py @@ -145,7 +145,7 @@ async def delete_model(pk: Annotated[int, Path(...)]) -> ResponseModel: return response_base.fail() -@router.get('/tables', summary='获取数据库表', dependencies=[DependsRBAC]) +@router.get('/tables', summary='获取数据库表') async def get_all_tables(table_schema: Annotated[str, Query(..., description='数据库名')] = 'fba') -> ResponseModel: data = await gen_service.get_tables(table_schema=table_schema) return response_base.success(data=data) diff --git a/backend/common/security/rbac.py b/backend/common/security/rbac.py index 0a8ec896..7cece194 100644 --- a/backend/common/security/rbac.py +++ b/backend/common/security/rbac.py @@ -21,7 +21,7 @@ async def enforcer() -> casbin.AsyncEnforcer: :return: """ - # 规则数据作为死数据直接在方法内定义 + # 模型定义:https://casbin.org/zh/docs/category/model _CASBIN_RBAC_MODEL_CONF_TEXT = """ [request_definition] r = sub, obj, act @@ -46,55 +46,73 @@ async def enforcer() -> casbin.AsyncEnforcer: async def rbac_verify(self, request: Request, _token: str = DependsJwtAuth) -> None: """ - RBAC 权限校验 + RBAC 权限校验(鉴权顺序很重要,谨慎修改) :param request: :param _token: :return: """ path = request.url.path - # 鉴权白名单 + + # API 鉴权白名单 if path in settings.TOKEN_REQUEST_PATH_EXCLUDE: return + # JWT 授权状态强制校验 if not request.auth.scopes: raise TokenError + # 超级管理员免校验 if request.user.is_superuser: return - # 检测角色数据权限范围 + + # 检测用户角色 user_roles = request.user.roles if not user_roles: - raise AuthorizationError(msg='用户未分配角色,授权失败') + raise AuthorizationError + + # 检测用户所属角色菜单 if not any(len(role.menus) > 0 for role in user_roles): - raise AuthorizationError(msg='用户所属角色未分配菜单,授权失败') + raise AuthorizationError + # 检测后台管理操作权限 method = request.method if method != MethodType.GET or method != MethodType.OPTIONS: if not request.user.is_staff: raise AuthorizationError(msg='用户已被禁止后台管理操作,请联系系统管理员') + # 数据权限范围 if any(role.data_scope == 1 for role in user_roles): return + # RBAC 鉴权 if settings.PERMISSION_MODE == 'role-menu': path_auth_perm = getattr(request.state, 'permission', None) - # 没有菜单权限标识不校验 + + # 没有菜单操作权限标识不校验 if not path_auth_perm: return + + # 菜单鉴权白名单 if path_auth_perm in settings.RBAC_ROLE_MENU_EXCLUDE: return + + # 已分配菜单权限校验 allow_perms = [] for role in user_roles: for menu in role.menus: - if menu.status == StatusType.enable: + if menu.perms and menu.status == StatusType.enable: allow_perms.extend(menu.perms.split(',')) if path_auth_perm not in allow_perms: raise AuthorizationError else: - user_uuid = request.user.uuid + # casbin 鉴权白名单 if (method, path) in settings.RBAC_CASBIN_EXCLUDE: return + + # casbin 权限校验 + # 实现机制:backend/app/admin/api/v1/sys/casbin.py + user_uuid = request.user.uuid enforcer = await self.enforcer() if not enforcer.enforce(user_uuid, path, method): raise AuthorizationError From 8210e106587f28e5d9a91d2993f9fc52540176fe Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 28 Oct 2024 18:43:54 +0800 Subject: [PATCH 5/7] update codes --- backend/app/admin/crud/crud_role.py | 19 ++++++++- backend/app/admin/crud/crud_user.py | 14 ++++--- backend/app/admin/model/sys_dept.py | 3 ++ backend/app/admin/model/sys_role.py | 8 +++- backend/app/admin/schema/role.py | 4 ++ backend/app/admin/service/role_service.py | 18 ++++++++- backend/common/security/jwt.py | 6 +-- backend/common/security/permission.py | 47 +++++++++++++++++++++++ backend/common/security/rbac.py | 4 -- 9 files changed, 105 insertions(+), 18 deletions(-) diff --git a/backend/app/admin/crud/crud_role.py b/backend/app/admin/crud/crud_role.py index f40e37e5..69d5119a 100644 --- a/backend/app/admin/crud/crud_role.py +++ b/backend/app/admin/crud/crud_role.py @@ -6,8 +6,8 @@ from sqlalchemy.orm import selectinload from sqlalchemy_crud_plus import CRUDPlus -from backend.app.admin.model import Menu, Role, User -from backend.app.admin.schema.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam +from backend.app.admin.model import Dept, Menu, Role, User +from backend.app.admin.schema.role import CreateRoleParam, UpdateRoleDeptParam, UpdateRoleMenuParam, UpdateRoleParam class CRUDRole(CRUDPlus[Role]): @@ -122,6 +122,21 @@ async def update_menus(self, db, role_id: int, menu_ids: UpdateRoleMenuParam) -> current_role.menus = menus.scalars().all() return len(current_role.menus) + async def update_depts(self, db, role_id: int, dept_ids: UpdateRoleDeptParam) -> int: + """ + 更新角色部门 + + :param db: + :param role_id: + :param dept_ids: + :return: + """ + stmt = select(Dept).where(Dept.id.in_(dept_ids.depts)) + depts = await db.execute(stmt) + current_role = await self.get_with_relation(db, role_id) + current_role.depts = depts.scalars().all() + return len(current_role.depts) + async def delete(self, db, role_id: list[int]) -> int: """ 删除角色 diff --git a/backend/app/admin/crud/crud_user.py b/backend/app/admin/crud/crud_user.py index c6bf35e7..04dee8d9 100644 --- a/backend/app/admin/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -183,8 +183,10 @@ async def get_list(self, dept: int = None, username: str = None, phone: str = No """ stmt = ( select(self.model) - .options(selectinload(self.model.dept)) - .options(selectinload(self.model.roles).selectinload(Role.menus)) + .options( + selectinload(self.model.dept), + selectinload(self.model.roles).selectinload(Role.menus), + ) .order_by(desc(self.model.join_time)) ) where_list = [] @@ -297,10 +299,10 @@ async def get_with_relation(self, db: AsyncSession, *, user_id: int = None, user :param username: :return: """ - stmt = ( - select(self.model) - .options(selectinload(self.model.dept)) - .options(selectinload(self.model.roles).joinedload(Role.menus)) + stmt = select(self.model).options( + selectinload(self.model.dept), + selectinload(self.model.roles).joinedload(Role.menus), + selectinload(self.model.roles).joinedload(Role.depts), ) filters = [] if user_id: diff --git a/backend/app/admin/model/sys_dept.py b/backend/app/admin/model/sys_dept.py index a85805d9..9bb5aabf 100644 --- a/backend/app/admin/model/sys_dept.py +++ b/backend/app/admin/model/sys_dept.py @@ -32,3 +32,6 @@ class Dept(Base): # 部门用户一对多 users: Mapped[list['User']] = relationship(init=False, back_populates='dept') # noqa: F821 + + # 部门角色多对多 + roles: Mapped[list['Role']] = relationship(init=False, back_populates='dept') # noqa: F821 diff --git a/backend/app/admin/model/sys_role.py b/backend/app/admin/model/sys_role.py index 73cbf8dd..6429bff3 100644 --- a/backend/app/admin/model/sys_role.py +++ b/backend/app/admin/model/sys_role.py @@ -4,7 +4,7 @@ from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.admin.model.m2m import sys_role_menu, sys_user_role +from backend.app.admin.model.m2m import sys_role_dept, sys_role_menu, sys_user_role from backend.common.model import Base, id_key @@ -16,7 +16,8 @@ class Role(Base): id: Mapped[id_key] = mapped_column(init=False) name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称') data_scope: Mapped[int | None] = mapped_column( - default=2, comment='权限范围(0: 全部数据,1: 本人数据,2: 所在部门数据,3: 所在部门及以下数据,4: 自定义数据)' + default=0, + comment='权限范围(0: 全部数据,1: 自定义数据,2: 所在部门及以下数据,3: 所在部门数据,4: 仅本人数据)', ) status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)') remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注') @@ -26,3 +27,6 @@ class Role(Base): # 角色菜单多对多 menus: Mapped[list['Menu']] = relationship(init=False, secondary=sys_role_menu, back_populates='roles') # noqa: F821 + + # 角色部门多对多 + depts: Mapped[list['Dept']] = relationship(init=False, secondary=sys_role_dept, back_populates='roles') # noqa: F821 diff --git a/backend/app/admin/schema/role.py b/backend/app/admin/schema/role.py index e1fd61ef..f5cbd7a5 100644 --- a/backend/app/admin/schema/role.py +++ b/backend/app/admin/schema/role.py @@ -30,6 +30,10 @@ class UpdateRoleMenuParam(SchemaBase): menus: list[int] +class UpdateRoleDeptParam(SchemaBase): + depts: list[int] + + class GetRoleListDetails(RoleSchemaBase): model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/admin/service/role_service.py b/backend/app/admin/service/role_service.py index d1335936..8f33ae84 100644 --- a/backend/app/admin/service/role_service.py +++ b/backend/app/admin/service/role_service.py @@ -5,10 +5,11 @@ from fastapi import Request from sqlalchemy import Select +from backend.app.admin.crud.crud_dept import dept_dao from backend.app.admin.crud.crud_menu import menu_dao from backend.app.admin.crud.crud_role import role_dao from backend.app.admin.model import Role -from backend.app.admin.schema.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam +from backend.app.admin.schema.role import CreateRoleParam, UpdateRoleDeptParam, UpdateRoleMenuParam, UpdateRoleParam from backend.common.exception import errors from backend.core.conf import settings from backend.database.db_mysql import async_db_session @@ -77,6 +78,21 @@ async def update_role_menu(*, request: Request, pk: int, menu_ids: UpdateRoleMen await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') return count + @staticmethod + async def update_role_dept(*, request: Request, pk: int, dept_ids: UpdateRoleDeptParam) -> int: + async with async_db_session.begin() as db: + role = await role_dao.get(db, pk) + if not role: + raise errors.NotFoundError(msg='角色不存在') + for dept_id in dept_ids.depts: + dept = await dept_dao.get(db, dept_id) + if not dept: + raise errors.NotFoundError(msg='部门不存在') + count = await role_dao.update_depts(db, pk, dept_ids) + if pk in [role.id for role in request.user.roles]: + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') + return count + @staticmethod async def delete(*, pk: list[int]) -> int: async with async_db_session.begin() as db: diff --git a/backend/common/security/jwt.py b/backend/common/security/jwt.py index 2fcc4bb1..259e2efc 100644 --- a/backend/common/security/jwt.py +++ b/backend/common/security/jwt.py @@ -183,13 +183,13 @@ async def get_current_user(db: AsyncSession, pk: int) -> User: raise AuthorizationError(msg='用户已被锁定,请联系系统管理员') if user.dept_id: if not user.dept.status: - raise AuthorizationError(msg='用户所属部门已锁定') + raise AuthorizationError(msg='用户所属部门已被锁定,请联系系统管理员') if user.dept.del_flag: - raise AuthorizationError(msg='用户所属部门已删除') + raise AuthorizationError(msg='用户所属部门已被删除,请联系系统管理员') if user.roles: role_status = [role.status for role in user.roles] if all(status == 0 for status in role_status): - raise AuthorizationError(msg='用户所属角色已锁定') + raise AuthorizationError(msg='用户所属角色已被锁定,请联系系统管理员') return user diff --git a/backend/common/security/permission.py b/backend/common/security/permission.py index 3db8b550..28d41368 100644 --- a/backend/common/security/permission.py +++ b/backend/common/security/permission.py @@ -1,7 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from typing import Any + from fastapi import Request +from sqlalchemy import Select, select +from backend.app.admin.model import User +from backend.app.admin.model.m2m import sys_role_dept from backend.common.exception.errors import ServerError from backend.core.conf import settings @@ -24,3 +29,45 @@ async def __call__(self, request: Request): raise ServerError # 附加权限标识 request.state.permission = self.value + + +def filter_user_data_scope(request: Request, model: Any, stmt: Select) -> Select: + """ + 获取用户数据范围 + + 使用场景:对于非后台管理数据,需要在前端界面向用户进行展示的数据 + + :param request: + :param model: + :param stmt: + :return: + """ + user_roles = request.user.roles + dept_roles = request.user.dept.roles + user_roles.extend(dept_roles) + data_scope = min(role.data_scope for role in set(user_roles)) + + # 全部数据 + if data_scope == 0: + stmt = stmt + # 自定义数据 + elif data_scope == 1: + stmt = stmt.where( + model.create_user.in_( + select(User.id) + .select_from(sys_role_dept) + .join(User, User.dept_id == sys_role_dept.c.dept_id) + .where(sys_role_dept.c.role_id.in_(user_roles)) + ) + ) + # 所在部门及以下数据 + elif data_scope == 2: + ... # TODO + # 所在部门数据 + elif data_scope == 3: + stmt = stmt.where(select(User.id).where(User.dept_id == request.user.dept_id)) + # 仅本人数据 + elif data_scope == 4: + stmt = stmt.where(model.create_user == request.user.id) + + return stmt diff --git a/backend/common/security/rbac.py b/backend/common/security/rbac.py index 7cece194..d6fbea9c 100644 --- a/backend/common/security/rbac.py +++ b/backend/common/security/rbac.py @@ -81,10 +81,6 @@ async def rbac_verify(self, request: Request, _token: str = DependsJwtAuth) -> N if not request.user.is_staff: raise AuthorizationError(msg='用户已被禁止后台管理操作,请联系系统管理员') - # 数据权限范围 - if any(role.data_scope == 1 for role in user_roles): - return - # RBAC 鉴权 if settings.PERMISSION_MODE == 'role-menu': path_auth_perm = getattr(request.state, 'permission', None) From 923861070419445f23c5e91ef4fd7e531d34e0e1 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 28 Oct 2024 18:57:46 +0800 Subject: [PATCH 6/7] add update role depts api --- backend/app/admin/api/v1/sys/role.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/app/admin/api/v1/sys/role.py b/backend/app/admin/api/v1/sys/role.py index e08d4edf..043553e1 100644 --- a/backend/app/admin/api/v1/sys/role.py +++ b/backend/app/admin/api/v1/sys/role.py @@ -4,7 +4,8 @@ from fastapi import APIRouter, Depends, Path, Query, Request -from backend.app.admin.schema.role import CreateRoleParam, GetRoleListDetails, UpdateRoleMenuParam, UpdateRoleParam +from backend.app.admin.schema.role import CreateRoleParam, GetRoleListDetails, UpdateRoleMenuParam, UpdateRoleParam, \ + UpdateRoleDeptParam from backend.app.admin.service.menu_service import menu_service from backend.app.admin.service.role_service import role_service from backend.common.pagination import DependsPagination, paging_data @@ -109,6 +110,23 @@ async def update_role_menus( return response_base.fail() +@router.put( + '/{pk}/dept', + summary='更新角色部门', + dependencies=[ + Depends(RequestPermission('sys:role:dept:edit')), + DependsRBAC, + ], +) +async def update_role_depts( + request: Request, pk: Annotated[int, Path(...)], dept_ids: UpdateRoleDeptParam +) -> ResponseModel: + count = await role_service.update_role_dept(request=request, pk=pk, menu_ids=dept_ids) + if count > 0: + return response_base.success() + return response_base.fail() + + @router.delete( '', summary='(批量)删除角色', From 95cb1bd83cb38398431ca68801a2f9473f857e11 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 11 Nov 2024 18:59:05 +0800 Subject: [PATCH 7/7] add comments --- backend/app/admin/api/v1/sys/role.py | 9 +++++++-- backend/common/security/permission.py | 25 ++++++++----------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/backend/app/admin/api/v1/sys/role.py b/backend/app/admin/api/v1/sys/role.py index 043553e1..ab8549e2 100644 --- a/backend/app/admin/api/v1/sys/role.py +++ b/backend/app/admin/api/v1/sys/role.py @@ -4,8 +4,13 @@ from fastapi import APIRouter, Depends, Path, Query, Request -from backend.app.admin.schema.role import CreateRoleParam, GetRoleListDetails, UpdateRoleMenuParam, UpdateRoleParam, \ - UpdateRoleDeptParam +from backend.app.admin.schema.role import ( + CreateRoleParam, + GetRoleListDetails, + UpdateRoleDeptParam, + UpdateRoleMenuParam, + UpdateRoleParam, +) from backend.app.admin.service.menu_service import menu_service from backend.app.admin.service.role_service import role_service from backend.common.pagination import DependsPagination, paging_data diff --git a/backend/common/security/permission.py b/backend/common/security/permission.py index 28d41368..f67417a1 100644 --- a/backend/common/security/permission.py +++ b/backend/common/security/permission.py @@ -3,10 +3,8 @@ from typing import Any from fastapi import Request -from sqlalchemy import Select, select +from sqlalchemy import Select -from backend.app.admin.model import User -from backend.app.admin.model.m2m import sys_role_dept from backend.common.exception.errors import ServerError from backend.core.conf import settings @@ -33,13 +31,13 @@ async def __call__(self, request: Request): def filter_user_data_scope(request: Request, model: Any, stmt: Select) -> Select: """ - 获取用户数据范围 + 筛选用户数据范围 使用场景:对于非后台管理数据,需要在前端界面向用户进行展示的数据 - :param request: - :param model: - :param stmt: + :param request: 接口请求对象 + :param model: 当前需要进行数据过滤的 sqlalchemy 模型 + :param stmt: 需要进行数据筛选的 stmt(select) 语句 :return: """ user_roles = request.user.roles @@ -50,22 +48,15 @@ def filter_user_data_scope(request: Request, model: Any, stmt: Select) -> Select # 全部数据 if data_scope == 0: stmt = stmt - # 自定义数据 + # 自定义数据(自选部门) elif data_scope == 1: - stmt = stmt.where( - model.create_user.in_( - select(User.id) - .select_from(sys_role_dept) - .join(User, User.dept_id == sys_role_dept.c.dept_id) - .where(sys_role_dept.c.role_id.in_(user_roles)) - ) - ) + ... # 所在部门及以下数据 elif data_scope == 2: ... # TODO # 所在部门数据 elif data_scope == 3: - stmt = stmt.where(select(User.id).where(User.dept_id == request.user.dept_id)) + ... # 仅本人数据 elif data_scope == 4: stmt = stmt.where(model.create_user == request.user.id)