fix: tighten rag permissions and area scope

This commit is contained in:
wren
2026-05-11 18:01:09 +08:00
parent f788149ca7
commit 2aa5a6d1d6
5 changed files with 189 additions and 12 deletions
@@ -53,6 +53,10 @@ class RagChatController(BaseController):
"message_feedback": "rag:message:feedback",
"app_read": "rag:app:read",
"dataset_read": "rag:dataset:read",
"dataset_manage": "rag:dataset:manage",
"dataset_create": "rag:dataset:create",
"dataset_update": "rag:dataset:update",
"dataset_delete": "rag:dataset:delete",
}
def __init__(self):
@@ -102,7 +106,7 @@ class RagChatController(BaseController):
pageSize: int = Query(20, ge=1, le=200),
payload: dict[str, Any] = Depends(verify_access_token),
):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_manage"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有管理知识库权限", "data": None})
data = await self.RagDatasetService.GetAdminDatasets(
CurrentUserId=int(payload["user_id"]),
@@ -117,7 +121,7 @@ class RagChatController(BaseController):
@self.router.post("/datasets/admin", response_model=Result[RagDatasetDetailVO])
async def CreateAdminDataset(Body: dict[str, Any], payload: dict[str, Any] = Depends(verify_access_token)):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_create"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有创建知识库权限", "data": None})
data = await self.RagDatasetService.CreateAdminDataset(
CurrentUserId=int(payload["user_id"]),
@@ -129,7 +133,7 @@ class RagChatController(BaseController):
@self.router.put("/datasets/admin/{DatasetId}", response_model=Result[RagDatasetDetailVO | None])
async def UpdateAdminDataset(DatasetId: int, Body: dict[str, Any], payload: dict[str, Any] = Depends(verify_access_token)):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_update"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有更新知识库权限", "data": None})
data = await self.RagDatasetService.UpdateAdminDataset(
CurrentUserId=int(payload["user_id"]),
@@ -142,7 +146,7 @@ class RagChatController(BaseController):
@self.router.delete("/datasets/admin/{DatasetId}", response_model=Result[RagOperationResultVO])
async def DeleteAdminDataset(DatasetId: int, payload: dict[str, Any] = Depends(verify_access_token)):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_delete"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有删除知识库权限", "data": None})
data = await self.RagDatasetService.DeleteAdminDataset(
CurrentUserId=int(payload["user_id"]),
@@ -166,7 +170,7 @@ class RagChatController(BaseController):
@self.router.patch("/datasets/{DatasetId}", response_model=Result[RagDatasetDetailVO | None])
async def UpdateDataset(DatasetId: int, Body: RagDatasetUpdateDTO, payload: dict[str, Any] = Depends(verify_access_token)):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_update"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有修改知识库权限", "data": None})
data = await self.RagDatasetService.UpdateDataset(
CurrentUserId=int(payload["user_id"]),
@@ -222,7 +226,7 @@ class RagChatController(BaseController):
data: str | None = Form(None),
payload: dict[str, Any] = Depends(verify_access_token),
):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_update"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有上传知识库文档权限", "data": None})
process_config = json.loads(data) if data else None
file_bytes = await file.read()
@@ -246,7 +250,7 @@ class RagChatController(BaseController):
data: str | None = Form(None),
payload: dict[str, Any] = Depends(verify_access_token),
):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_update"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有重处理知识库文档权限", "data": None})
process_config = json.loads(data) if data else None
file_bytes = await file.read()
@@ -287,7 +291,7 @@ class RagChatController(BaseController):
Body: dict[str, Any],
payload: dict[str, Any] = Depends(verify_access_token),
):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_update"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有修改知识库文档状态权限", "data": None})
enabled = Action == "enable"
if Action not in {"enable", "disable"}:
@@ -332,7 +336,7 @@ class RagChatController(BaseController):
DocumentId: int,
payload: dict[str, Any] = Depends(verify_access_token),
):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_delete"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有删除知识库文档权限", "data": None})
result = await self.RagDatasetService.DeleteDatasetDocument(
CurrentUserId=int(payload["user_id"]),
@@ -388,7 +392,7 @@ class RagChatController(BaseController):
Body: dict[str, Any],
payload: dict[str, Any] = Depends(verify_access_token),
):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_update"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有修改知识库分段权限", "data": None})
result = await self.RagDatasetService.UpdateDatasetDocumentSegment(
CurrentUserId=int(payload["user_id"]),
@@ -408,7 +412,7 @@ class RagChatController(BaseController):
SegmentId: str,
payload: dict[str, Any] = Depends(verify_access_token),
):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_read"]]):
if not await self._check_permission(int(payload["user_id"]), [self._PERMISSIONS["dataset_delete"]]):
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有删除知识库分段权限", "data": None})
result = await self.RagDatasetService.DeleteDatasetDocumentSegment(
CurrentUserId=int(payload["user_id"]),
@@ -65,6 +65,7 @@ class RagDatasetServiceImpl(IRagDatasetService):
) -> RagDatasetPageVO:
if UserRole not in ("provincial_admin", "admin", "super_admin"):
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户没有管理知识库权限")
managed_area = self._resolve_managed_area(UserRole=UserRole, UserArea=UserArea)
filters = ["d.deleted_at IS NULL"]
params: dict = {
@@ -72,7 +73,12 @@ class RagDatasetServiceImpl(IRagDatasetService):
"limit": PageSize,
}
areas = [item.strip() for item in str(Area or "").split(",") if item.strip()]
if len(areas) == 1:
if managed_area:
if areas and any(item != managed_area for item in areas):
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户只能查看本地区知识库配置")
filters.append("d.area = :managed_area")
params["managed_area"] = managed_area
elif len(areas) == 1:
filters.append("d.area = :area")
params["area"] = areas[0]
elif len(areas) > 1:
@@ -127,6 +133,7 @@ class RagDatasetServiceImpl(IRagDatasetService):
description = str(Body.get("dataset_description") or Body.get("description") or "").strip()
if not area or not name:
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "地区和知识库名称不能为空")
self._assert_manage_area_scope(UserRole=UserRole, UserArea=UserArea, DatasetArea=area)
collection_name = self._slugify_collection_name(area, name)
retrieval_model = {}
@@ -208,8 +215,10 @@ class RagDatasetServiceImpl(IRagDatasetService):
existing = await self._get_dataset_row(DatasetId)
if not existing:
return None
self._assert_manage_area_scope(UserRole=UserRole, UserArea=UserArea, DatasetArea=str(existing.get("area") or ""))
area = str(Body.get("area") or existing.get("area") or "").strip()
self._assert_manage_area_scope(UserRole=UserRole, UserArea=UserArea, DatasetArea=area)
async with GetAsyncSession() as session:
target_is_default = bool(Body.get("is_default", existing.get("is_default")))
@@ -268,6 +277,7 @@ class RagDatasetServiceImpl(IRagDatasetService):
existing = await self._get_dataset_row(DatasetId)
if not existing:
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "知识库不存在")
self._assert_manage_area_scope(UserRole=UserRole, UserArea=UserArea, DatasetArea=str(existing.get("area") or ""))
if bool(existing.get("is_default")):
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "默认知识库不允许删除,请先切换默认知识库")
async with GetAsyncSession() as session:
@@ -691,6 +701,24 @@ class RagDatasetServiceImpl(IRagDatasetService):
return f"legal_kb_{normalized}"[:96]
return f"legal_kb_{uuid.uuid4().hex[:12]}"
def _resolve_managed_area(self, UserRole: str | None, UserArea: str | None) -> str | None:
if UserRole == "admin":
area = str(UserArea or "").strip()
if not area:
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前市级管理员未配置地区,无法管理知识库")
return area
return None
def _assert_manage_area_scope(self, UserRole: str | None, UserArea: str | None, DatasetArea: str) -> None:
if UserRole in ("provincial_admin", "super_admin"):
return
if UserRole != "admin":
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户没有管理知识库权限")
managed_area = self._resolve_managed_area(UserRole=UserRole, UserArea=UserArea)
if DatasetArea != managed_area:
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前用户只能管理本地区知识库")
async def UploadDatasetDocument(
self,
CurrentUserId: int,
@@ -252,6 +252,10 @@ class RbacAdminServiceImpl(IRbacAdminService):
{"permission_key": "rag:conversation:delete", "display_name": "删除 RAG 会话", "module": "rag", "resource": "conversation", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/rag/chat/conversations/{ConversationId}", "route_path": "/chat-with-llm"},
{"permission_key": "rag:message:feedback", "display_name": "反馈 RAG 消息", "module": "rag", "resource": "message", "action": "feedback", "api_method": "POST", "api_path": "/api/v3/rag/chat/messages/{MessageId}/feedback", "route_path": "/chat-with-llm"},
{"permission_key": "rag:dataset:read", "display_name": "查看 RAG 知识库", "module": "rag", "resource": "dataset", "action": "read", "api_method": "GET", "api_path": "/api/v3/rag/datasets/my", "route_path": "/chat-with-llm"},
{"permission_key": "rag:dataset:manage", "display_name": "查看知识库配置管理", "module": "rag", "resource": "dataset", "action": "manage", "api_method": "GET", "api_path": "/api/v3/rag/datasets/admin", "route_path": "/chat-with-llm"},
{"permission_key": "rag:dataset:create", "display_name": "创建知识库", "module": "rag", "resource": "dataset", "action": "create", "api_method": "POST", "api_path": "/api/v3/rag/datasets/admin", "route_path": "/chat-with-llm"},
{"permission_key": "rag:dataset:update", "display_name": "更新知识库与文档", "module": "rag", "resource": "dataset", "action": "update", "api_method": "PATCH", "api_path": "/api/v3/rag/datasets/{DatasetId}", "route_path": "/chat-with-llm"},
{"permission_key": "rag:dataset:delete", "display_name": "删除知识库与文档", "module": "rag", "resource": "dataset", "action": "delete", "api_method": "DELETE", "api_path": "/api/v3/rag/datasets/admin/{DatasetId}", "route_path": "/chat-with-llm"},
]
async def ListRoles(self, CurrentUserId: int, Page: int, PageSize: int, RoleKey: str | None, RoleName: str | None, IncludeSystem: bool) -> RoleListVO:
@@ -571,6 +571,7 @@ class RbacServiceImpl(IRbacService):
}
_PERMISSION_PREFIXES_BY_PATH: dict[str, list[str]] = {
"/chat-with-llm": ["rag:"],
"/files": ["documents:"],
"/files/upload": ["documents:upload:"],
"/documents": ["documents:"],