authorization_service.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. """主案、子方案及共享附件的集中读取授权。"""
  2. from __future__ import annotations
  3. import logging
  4. from dataclasses import dataclass
  5. from datetime import datetime, timezone
  6. from sqlalchemy import or_, select
  7. from dms.common.enums import (
  8. AllowedAction,
  9. DeptScope,
  10. DocumentStatus,
  11. DocumentType,
  12. EnabledStatus,
  13. RoleCode,
  14. SECURITY_LEVEL_VALUES,
  15. )
  16. from dms.extensions import db
  17. from dms.models import (
  18. AttachmentBinding,
  19. Document,
  20. Organization,
  21. Permission,
  22. Role,
  23. RolePermission,
  24. User,
  25. )
  26. from dms.security.auth_context import AuthContext
  27. from dms.common.errors import (
  28. DocumentActionForbiddenError,
  29. DocumentDownloadForbiddenError,
  30. DocumentViewForbiddenError,
  31. SecurityLevelForbiddenError,
  32. )
  33. from dms.services.admin_permission import SUPER_ADMIN_USERNAME
  34. logger = logging.getLogger(__name__)
  35. # 矩阵布尔位与 AllowedAction 的一一对应
  36. MATRIX_ACTION_COLUMNS: tuple[tuple[str, AllowedAction], ...] = (
  37. ("can_view", AllowedAction.VIEW),
  38. ("can_download", AllowedAction.DOWNLOAD),
  39. ("can_edit", AllowedAction.EDIT),
  40. ("can_delete", AllowedAction.DELETE),
  41. ("can_config_permission", AllowedAction.CONFIG_PERMISSION),
  42. ("can_bind_attachment", AllowedAction.BIND_ATTACHMENT),
  43. ("can_unbind_attachment", AllowedAction.UNBIND_ATTACHMENT),
  44. )
  45. @dataclass(frozen=True, slots=True)
  46. class PlanAccess:
  47. allowed: bool
  48. reason: str | None
  49. source: Document | None
  50. permissions: tuple[Permission, ...]
  51. can_download: bool
  52. matrix_actions: frozenset[str] = frozenset()
  53. class AccessMemo:
  54. """请求级授权备忘:单次列表批量评估共享,缓存请求内不变量
  55. (生效角色、机构路径、矩阵行、祖先机构、权限源主案、授权列表),
  56. 消除逐行重复查库(对齐 Java 端 AccessMemo)。
  57. 仅限同一请求、同一用户上下文内使用,禁止跨请求复用——
  58. 备忘不感知请求期间的权限数据变更。
  59. """
  60. __slots__ = (
  61. "role_loaded",
  62. "role",
  63. "org_paths_by_id",
  64. "matrix_actions_by_key",
  65. "roots_by_id",
  66. "permissions_by_source_id",
  67. )
  68. def __init__(self) -> None:
  69. # 生效角色是否已加载(role 允许为 None:无生效角色)
  70. self.role_loaded = False
  71. self.role: Role | None = None
  72. # 机构 org_path 按 id 缓存(dept_scope 逐行判定复用)
  73. self.org_paths_by_id: dict[int, str | None] = {}
  74. # 矩阵操作位按 (security_level, dept_scope) 缓存
  75. self.matrix_actions_by_key: dict[tuple[str, str], frozenset[str]] = {}
  76. # SUB_PLAN 权限源主案按 root_id 缓存(同根子案批量评估复用)
  77. self.roots_by_id: dict[int, Document | None] = {}
  78. # 授权列表按权限源文档 id 缓存(evaluate_plan_access 与权限摘要共享)
  79. self.permissions_by_source_id: dict[int, list[Permission]] = {}
  80. def _security_allowed(document: Document, context: AuthContext) -> bool:
  81. return (
  82. SECURITY_LEVEL_VALUES[context.security_level]
  83. >= SECURITY_LEVEL_VALUES[type(context.security_level)(document.security_level)]
  84. )
  85. def _is_super_admin(context: AuthContext) -> bool:
  86. """内置超管(username == admin):文档操作不受角色矩阵限制。
  87. 其余 ADMIN(受限管理员)文档权限与 USER 同口径,按关联角色的
  88. 权限矩阵(密级×部门范围×操作位)判定。
  89. """
  90. return (
  91. context.role_code == RoleCode.ADMIN
  92. and context.username == SUPER_ADMIN_USERNAME
  93. )
  94. def require_security_clearance(
  95. document: Document,
  96. context: AuthContext,
  97. ) -> None:
  98. """集中执行文档密级前置检查,ADMIN也不能绕过。"""
  99. if not _security_allowed(document, context):
  100. raise SecurityLevelForbiddenError()
  101. # 写路径操作码 → 中文动词(拒绝提示与前端口径一致)
  102. ACTION_LABELS: dict[str, str] = {
  103. AllowedAction.VIEW.value: "查看",
  104. AllowedAction.DOWNLOAD.value: "下载",
  105. AllowedAction.EDIT.value: "编辑",
  106. AllowedAction.DELETE.value: "删除",
  107. AllowedAction.CONFIG_PERMISSION.value: "授权",
  108. AllowedAction.BIND_ATTACHMENT.value: "挂载",
  109. AllowedAction.UNBIND_ATTACHMENT.value: "解除挂载",
  110. }
  111. def require_plan_action(
  112. document: Document,
  113. context: AuthContext,
  114. action: AllowedAction,
  115. ) -> PlanAccess | None:
  116. """写路径集中鉴权:密级前置 + 操作位校验(契约1.36完全拆分)。
  117. 与读路径/前端按钮同源(evaluate_plan_access + plan_allowed_actions),
  118. 保证"按钮可点 ⟺ 后端放行";各操作位独立判定,查看不再是其他位的
  119. 前置:
  120. - 密级不足:任何角色(含超管)一律拒绝;
  121. - 共享附件:矩阵不授予编辑/删除/授权/挂载位,仅 ADMIN(含超管)
  122. 可维护;
  123. - 主案/子方案:超管恒过(密级通过后);受限 ADMIN 与 USER 由矩阵
  124. 操作位判定,子方案自动过滤授权/挂载/解除挂载位(继承自主案)。
  125. """
  126. if not _security_allowed(document, context):
  127. raise SecurityLevelForbiddenError()
  128. label = ACTION_LABELS.get(action.value, "执行该操作")
  129. if document.document_type == DocumentType.ATTACHMENT.value:
  130. if context.role_code != RoleCode.ADMIN:
  131. raise DocumentActionForbiddenError(
  132. f"无权{label}该共享附件:仅管理员可维护共享附件"
  133. )
  134. return None
  135. access = evaluate_plan_access(document, context)
  136. actions = plan_allowed_actions(document, context, access)
  137. if action.value not in actions:
  138. raise DocumentActionForbiddenError(
  139. f"无权{label}该文档:角色权限未授予该操作"
  140. )
  141. return access
  142. def _permission_source(
  143. document: Document,
  144. memo: AccessMemo | None = None,
  145. ) -> Document | None:
  146. if document.document_type == DocumentType.MAIN.value:
  147. return document
  148. if document.document_type != DocumentType.SUB_PLAN.value:
  149. return None
  150. if memo is not None and document.root_document_id in memo.roots_by_id:
  151. return memo.roots_by_id[document.root_document_id]
  152. root = db.session.scalar(
  153. select(Document).where(
  154. Document.id == document.root_document_id,
  155. Document.document_type == DocumentType.MAIN.value,
  156. Document.is_deleted.is_(False),
  157. )
  158. )
  159. if root is None:
  160. logger.error(
  161. "子方案数据一致性异常:document_id=%s root_document_id=%s",
  162. document.id,
  163. document.root_document_id,
  164. )
  165. if memo is not None:
  166. memo.roots_by_id[document.root_document_id] = root
  167. return root
  168. def _effective_role(
  169. context: AuthContext,
  170. memo: AccessMemo | None = None,
  171. ) -> Role | None:
  172. """实时解析 USER 体系的生效角色:显式角色优先,未挂角色回退默认角色。
  173. 显式角色被停用或软删时不回退默认角色(矩阵贡献为空)。
  174. """
  175. if memo is not None and memo.role_loaded:
  176. return memo.role
  177. role_id = db.session.scalar(
  178. select(User.role_id).where(
  179. User.id == context.user_id,
  180. User.is_deleted.is_(False),
  181. )
  182. )
  183. if role_id is not None:
  184. role = db.session.scalar(
  185. select(Role).where(
  186. Role.id == role_id,
  187. Role.is_deleted.is_(False),
  188. Role.status == EnabledStatus.ENABLED.value,
  189. )
  190. )
  191. else:
  192. role = db.session.scalar(
  193. select(Role).where(
  194. Role.is_default.is_(True),
  195. Role.is_deleted.is_(False),
  196. Role.status == EnabledStatus.ENABLED.value,
  197. )
  198. )
  199. if memo is not None:
  200. memo.role_loaded = True
  201. memo.role = role
  202. return role
  203. def _org_path(organization_id: int, memo: AccessMemo | None) -> str | None:
  204. if memo is not None and organization_id in memo.org_paths_by_id:
  205. return memo.org_paths_by_id[organization_id]
  206. path = db.session.scalar(
  207. select(Organization.org_path).where(
  208. Organization.id == organization_id,
  209. Organization.is_deleted.is_(False),
  210. )
  211. )
  212. if memo is not None:
  213. memo.org_paths_by_id[organization_id] = path
  214. return path
  215. def _dept_scope(
  216. source: Document,
  217. context: AuthContext,
  218. memo: AccessMemo | None = None,
  219. ) -> DeptScope:
  220. """按文档归属机构划分 OWN / OTHER(OTHER 涵盖所属部门子树外全部部门)。"""
  221. if source.organization_id is None:
  222. return DeptScope.OTHER
  223. if context.organization_id is not None:
  224. if memo is not None:
  225. doc_path = _org_path(source.organization_id, memo)
  226. user_path = _org_path(context.organization_id, memo)
  227. else:
  228. paths = db.session.execute(
  229. select(Organization.id, Organization.org_path).where(
  230. Organization.id.in_(
  231. {source.organization_id, context.organization_id}
  232. ),
  233. Organization.is_deleted.is_(False),
  234. )
  235. ).all()
  236. by_id = {organization_id: path for organization_id, path in paths}
  237. doc_path = by_id.get(source.organization_id)
  238. user_path = by_id.get(context.organization_id)
  239. if doc_path and user_path:
  240. own_prefix = user_path if user_path.endswith("/") else user_path + "/"
  241. if doc_path == user_path or doc_path.startswith(own_prefix):
  242. return DeptScope.OWN
  243. return DeptScope.OTHER
  244. def _matrix_actions(
  245. source: Document,
  246. context: AuthContext,
  247. memo: AccessMemo | None = None,
  248. ) -> frozenset[str]:
  249. """取生效角色矩阵中命中的操作位集合(密级×部门范围,实时查库)。"""
  250. role = _effective_role(context, memo)
  251. if role is None:
  252. return frozenset()
  253. scope = _dept_scope(source, context, memo)
  254. cache_key = (source.security_level, scope.value)
  255. if memo is not None and cache_key in memo.matrix_actions_by_key:
  256. return memo.matrix_actions_by_key[cache_key]
  257. row = db.session.scalar(
  258. select(RolePermission).where(
  259. RolePermission.role_id == role.id,
  260. RolePermission.security_level == source.security_level,
  261. RolePermission.dept_scope == scope.value,
  262. RolePermission.is_deleted.is_(False),
  263. )
  264. )
  265. if row is None:
  266. actions = frozenset()
  267. else:
  268. actions = frozenset(
  269. action.value
  270. for column, action in MATRIX_ACTION_COLUMNS
  271. if getattr(row, column)
  272. )
  273. if memo is not None:
  274. memo.matrix_actions_by_key[cache_key] = actions
  275. return actions
  276. def _active_permissions(
  277. source_id: int,
  278. memo: AccessMemo | None = None,
  279. ) -> list[Permission]:
  280. """权限源文档的授权列表(evaluate_plan_access 与权限摘要共享缓存)。"""
  281. if memo is not None and source_id in memo.permissions_by_source_id:
  282. return memo.permissions_by_source_id[source_id]
  283. permissions = list(
  284. db.session.scalars(
  285. select(Permission).where(
  286. Permission.document_id == source_id,
  287. Permission.is_deleted.is_(False),
  288. )
  289. ).all()
  290. )
  291. if memo is not None:
  292. memo.permissions_by_source_id[source_id] = permissions
  293. return permissions
  294. def evaluate_plan_access(
  295. document: Document,
  296. context: AuthContext,
  297. memo: AccessMemo | None = None,
  298. ) -> PlanAccess:
  299. """按固定顺序判断方案读取权限并返回有效权限快照。"""
  300. source = _permission_source(document, memo)
  301. if source is None:
  302. return PlanAccess(False, "INVALID_ROOT", None, (), False)
  303. if (
  304. context.role_code == RoleCode.USER
  305. and document.document_status != DocumentStatus.PUBLISHED.value
  306. ):
  307. return PlanAccess(False, "STATUS", source, (), False)
  308. if not _security_allowed(document, context):
  309. return PlanAccess(False, "SECURITY", source, (), False)
  310. # 超管直接放行;受限 ADMIN 落入下方矩阵+ACL 路径(与 USER 同口径,
  311. # 但不受发布状态限制——状态豁免仍是管理员语义)
  312. if _is_super_admin(context):
  313. return PlanAccess(True, None, source, (), True)
  314. matrix_actions = _matrix_actions(source, context, memo)
  315. # 借阅语义(0026):ACL 仅面向用户且限时生效,到期即失效(惰性判定,
  316. # 无需定时任务);矩阵与单文档借阅 ACL 为 OR 叠加
  317. now = datetime.now(timezone.utc).replace(tzinfo=None)
  318. matched = [
  319. permission
  320. for permission in _active_permissions(source.id, memo)
  321. if permission.subject_id == context.user_id
  322. and permission.expires_at > now
  323. ]
  324. can_view = any(permission.can_view for permission in matched)
  325. can_view = can_view or AllowedAction.VIEW.value in matrix_actions
  326. # 完全拆分(契约1.36):下载独立判定,不再以查看为前置——
  327. # 借阅ACL只勾下载、矩阵只勾下载均直接生效
  328. can_download = any(
  329. permission.can_download for permission in matched
  330. ) or AllowedAction.DOWNLOAD.value in matrix_actions
  331. return PlanAccess(
  332. can_view,
  333. None if can_view else "ACL",
  334. source,
  335. tuple(matched),
  336. can_download,
  337. matrix_actions,
  338. )
  339. def plan_allowed_actions(
  340. document: Document,
  341. context: AuthContext,
  342. access: PlanAccess,
  343. ) -> list[str]:
  344. if not _is_super_admin(context):
  345. # 完全拆分(契约1.36):各操作位独立判定,查看不再是其他位的
  346. # 前置——VIEW/DOWNLOAD 按命中来源(矩阵位或借阅ACL)独立下发,
  347. # 矩阵操作位直接叠加;子方案不适用授权/挂载/解除挂载
  348. actions: list[str] = []
  349. if access.allowed:
  350. actions.append(AllowedAction.VIEW.value)
  351. if access.can_download:
  352. actions.append(AllowedAction.DOWNLOAD.value)
  353. applicable = None
  354. if document.document_type == DocumentType.SUB_PLAN.value:
  355. applicable = {
  356. AllowedAction.VIEW.value,
  357. AllowedAction.DOWNLOAD.value,
  358. AllowedAction.EDIT.value,
  359. AllowedAction.DELETE.value,
  360. }
  361. for action in access.matrix_actions:
  362. if applicable is not None and action not in applicable:
  363. continue
  364. if action not in actions:
  365. actions.append(action)
  366. return actions
  367. if document.document_type == DocumentType.SUB_PLAN.value:
  368. return [
  369. AllowedAction.VIEW.value,
  370. AllowedAction.DOWNLOAD.value,
  371. AllowedAction.EDIT.value,
  372. AllowedAction.DELETE.value,
  373. ]
  374. # 阶段11方案B:ADMIN恒含UNBIND(与BIND对称)。原"存在附件挂载才有位"
  375. # 是UI显影残留——附件表零挂载不渲染、子案挂载(0018)复用该位后反而出现
  376. # "仅有子案挂载的主案管理员无法解挂"的倒挂,故移除条件(顺带消除
  377. # 主案列表每行一次的count查询)。
  378. return [
  379. AllowedAction.VIEW.value,
  380. AllowedAction.DOWNLOAD.value,
  381. AllowedAction.EDIT.value,
  382. AllowedAction.CONFIG_PERMISSION.value,
  383. AllowedAction.DELETE.value,
  384. AllowedAction.BIND_ATTACHMENT.value,
  385. AllowedAction.UNBIND_ATTACHMENT.value,
  386. ]
  387. def attachment_allowed_actions(
  388. context: AuthContext,
  389. access: PlanAccess | None = None,
  390. ) -> list[str]:
  391. if context.role_code == RoleCode.ADMIN:
  392. return [
  393. AllowedAction.VIEW.value,
  394. AllowedAction.DOWNLOAD.value,
  395. AllowedAction.EDIT.value,
  396. AllowedAction.DELETE.value,
  397. ]
  398. actions: list[str] = []
  399. if access is not None and access.allowed:
  400. actions.append(AllowedAction.VIEW.value)
  401. # 下载独立下发(契约1.36完全拆分):无查看命中也可下载
  402. if access is not None and access.can_download:
  403. actions.append(AllowedAction.DOWNLOAD.value)
  404. return actions
  405. def evaluate_attachment_access(
  406. attachment: Document,
  407. context: AuthContext,
  408. ) -> tuple[bool, bool]:
  409. """USER只能通过至少一个有权有效主案访问共享附件。
  410. 阶段11:附件允许携带任意密级(0020放宽shape约束,存量子案保留原密级),
  411. 读路径密级前置与主案/子方案同口径——任何角色(含ADMIN)密级不足即拒绝。
  412. """
  413. if not _security_allowed(attachment, context):
  414. return False, False
  415. if context.role_code == RoleCode.ADMIN:
  416. return True, True
  417. mains = db.session.scalars(
  418. select(Document)
  419. .join(
  420. AttachmentBinding,
  421. AttachmentBinding.main_document_id == Document.id,
  422. )
  423. .where(
  424. AttachmentBinding.attachment_document_id == attachment.id,
  425. AttachmentBinding.is_deleted.is_(False),
  426. Document.document_type == DocumentType.MAIN.value,
  427. Document.is_deleted.is_(False),
  428. )
  429. .order_by(Document.id.asc())
  430. ).all()
  431. can_view = False
  432. can_download = False
  433. memo = AccessMemo()
  434. for main in mains:
  435. access = evaluate_plan_access(main, context, memo)
  436. can_view = can_view or access.allowed
  437. # 下载独立聚合(契约1.36完全拆分):任一有效主案命中下载即可
  438. can_download = can_download or access.can_download
  439. return can_view, can_download
  440. def require_attachment_access(
  441. attachment: Document,
  442. context: AuthContext,
  443. *,
  444. download: bool,
  445. ) -> tuple[bool, bool]:
  446. # 密级不足优先按密级拒绝(前端据此提示"密级不足"而非"权限不足")
  447. if not _security_allowed(attachment, context):
  448. raise SecurityLevelForbiddenError()
  449. can_view, can_download = evaluate_attachment_access(attachment, context)
  450. if download and not can_download:
  451. raise DocumentDownloadForbiddenError()
  452. if not download and not can_view:
  453. raise DocumentViewForbiddenError()
  454. return can_view, can_download
  455. # 方案权限矩阵「管理类」列(契约1.35菜单口径):编辑/删除/授权/挂载
  456. # (含解除挂载)与上传,任一勾选即渲染方案管理页签。
  457. PLAN_MANAGE_MATRIX_COLUMNS: tuple[str, ...] = (
  458. "can_edit",
  459. "can_delete",
  460. "can_config_permission",
  461. "can_bind_attachment",
  462. "can_unbind_attachment",
  463. "can_upload",
  464. )
  465. # 方案权限矩阵「浏览类」列(契约1.35菜单口径):无管理类权限时任一
  466. # 勾选渲染方案浏览页签。
  467. VIEW_MATRIX_COLUMNS: tuple[str, ...] = ("can_view", "can_download")
  468. def _organization_subtree_ids(organization_id: int | None) -> set[int]:
  469. """用户机构的下属子树(含自身),与 _dept_scope 的 OWN 判定同口径。"""
  470. if organization_id is None:
  471. return set()
  472. organizations = db.session.scalars(
  473. select(Organization).where(
  474. Organization.is_deleted.is_(False),
  475. Organization.status == EnabledStatus.ENABLED.value,
  476. )
  477. ).all()
  478. by_id = {organization.id: organization for organization in organizations}
  479. root = by_id.get(organization_id)
  480. if root is None:
  481. return set()
  482. prefix = root.org_path if root.org_path.endswith("/") else root.org_path + "/"
  483. return {
  484. organization.id
  485. for organization in organizations
  486. if organization.id == organization_id
  487. or (organization.org_path or "").startswith(prefix)
  488. }
  489. def has_any_matrix_permission(
  490. context: AuthContext, columns: tuple[str, ...]
  491. ) -> bool:
  492. """纯矩阵判定(契约1.35菜单口径):生效角色活跃矩阵行勾选指定列任一。
  493. 与文档是否存在无关——菜单可见性完全由角色权限决定;页签内按钮
  494. 与接口鉴权仍按 evaluate_plan_access 实时判定。
  495. """
  496. role = _effective_role(context)
  497. if role is None:
  498. return False
  499. row_id = db.session.scalar(
  500. select(RolePermission.id).where(
  501. RolePermission.role_id == role.id,
  502. RolePermission.is_deleted.is_(False),
  503. or_(
  504. *(
  505. getattr(RolePermission, column).is_(True)
  506. for column in columns
  507. )
  508. ),
  509. ).limit(1)
  510. )
  511. return row_id is not None
  512. def can_upload_plan(
  513. context: AuthContext,
  514. security_level: str,
  515. organization_id: int | None,
  516. ) -> bool:
  517. """主案上传权限(契约1.31):方案权限矩阵 can_upload 列。
  518. 超管恒可;文档归属本部门子树(或无归属)按 OWN 行判定,
  519. 归属其他部门按 OTHER 行判定(矩阵校验 OTHER 行 can_upload 恒 0,
  520. 即跨部门上传恒拒绝)。
  521. """
  522. if _is_super_admin(context):
  523. return True
  524. role = _effective_role(context)
  525. if role is None:
  526. return False
  527. own_ids = _organization_subtree_ids(context.organization_id)
  528. scope = (
  529. DeptScope.OWN
  530. if organization_id is None or organization_id in own_ids
  531. else DeptScope.OTHER
  532. )
  533. row = db.session.scalar(
  534. select(RolePermission).where(
  535. RolePermission.role_id == role.id,
  536. RolePermission.security_level == security_level,
  537. RolePermission.dept_scope == scope.value,
  538. RolePermission.is_deleted.is_(False),
  539. )
  540. )
  541. return row is not None and bool(row.can_upload)
  542. def has_any_upload_permission(role_id: int | None) -> bool:
  543. """会话 permUpload 位矩阵查询(契约1.31):任一 OWN 行 can_upload=True。
  544. 仅查生效角色的矩阵行,超管特判由调用方三元式处理(与既有 perm 位
  545. 组装同风格)。
  546. """
  547. if role_id is None:
  548. return False
  549. row = db.session.scalar(
  550. select(RolePermission).where(
  551. RolePermission.role_id == role_id,
  552. RolePermission.dept_scope == DeptScope.OWN.value,
  553. RolePermission.can_upload.is_(True),
  554. RolePermission.is_deleted.is_(False),
  555. )
  556. )
  557. return row is not None