role_service.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. """角色 CRUD、可访问部门、权限矩阵与用户关联维护。"""
  2. from __future__ import annotations
  3. from datetime import datetime, timezone
  4. from typing import Any
  5. from sqlalchemy import func, select
  6. from sqlalchemy.exc import IntegrityError
  7. from dms.common.enums import (
  8. AuditAction,
  9. AuditTarget,
  10. DeptScope,
  11. EnabledStatus,
  12. RoleCode,
  13. SecurityLevel,
  14. )
  15. from dms.common.errors import (
  16. ConflictError,
  17. InvalidArgumentError,
  18. ResourceNotFoundError,
  19. RoleInUseError,
  20. )
  21. from dms.common.pagination import PageRequest, page_result
  22. from dms.common.response import serialize_id
  23. from dms.database.transaction import transaction
  24. from dms.extensions import db
  25. from dms.models import Organization, Role, RoleDepartment, RolePermission, User
  26. from dms.security.auth_context import get_auth_context
  27. from dms.services.audit_service import business_audit
  28. ROLE_CREATE_FIELDS = {"roleName", "sortNo", "status", "remark", "isDefault"}
  29. ROLE_UPDATE_FIELDS = {
  30. "roleName",
  31. "sortNo",
  32. "status",
  33. "remark",
  34. "isDefault",
  35. "rowVersion",
  36. }
  37. ROLE_DEPARTMENT_FIELDS = {"organizationIds", "rowVersion"}
  38. ROLE_PERMISSION_FIELDS = {"matrix", "rowVersion"}
  39. ROLE_USER_FIELDS = {"userIds", "rowVersion"}
  40. # 矩阵 7 个操作布尔位与 AllowedAction 一一对应
  41. PERMISSION_FIELDS: tuple[tuple[str, str], ...] = (
  42. ("canView", "can_view"),
  43. ("canDownload", "can_download"),
  44. ("canEdit", "can_edit"),
  45. ("canDelete", "can_delete"),
  46. ("canConfigPermission", "can_config_permission"),
  47. ("canBindAttachment", "can_bind_attachment"),
  48. ("canUnbindAttachment", "can_unbind_attachment"),
  49. )
  50. MATRIX_KEYS = {
  51. (level.value, scope.value)
  52. for level in SecurityLevel
  53. for scope in DeptScope
  54. }
  55. def _utc_now() -> datetime:
  56. return datetime.now(timezone.utc).replace(tzinfo=None)
  57. def _required_text(
  58. value: Any,
  59. *,
  60. field: str,
  61. max_length: int,
  62. ) -> str:
  63. if not isinstance(value, str) or not value.strip():
  64. raise InvalidArgumentError(f"{field}不能为空")
  65. normalized = value.strip()
  66. if len(normalized) > max_length:
  67. raise InvalidArgumentError(f"{field}长度不能超过{max_length}")
  68. return normalized
  69. def _optional_text(value: Any, *, field: str, max_length: int) -> str | None:
  70. if value is None:
  71. return None
  72. if not isinstance(value, str):
  73. raise InvalidArgumentError(f"{field}必须是字符串")
  74. normalized = value.strip()
  75. if len(normalized) > max_length:
  76. raise InvalidArgumentError(f"{field}长度不能超过{max_length}")
  77. return normalized or None
  78. def _sort_no(value: Any) -> int:
  79. if type(value) is not int or value < 0 or value > 999999:
  80. raise InvalidArgumentError("sortNo必须是0~999999的整数")
  81. return value
  82. def _status_value(value: Any) -> str:
  83. if not isinstance(value, str):
  84. raise InvalidArgumentError("status必须是字符串")
  85. try:
  86. return EnabledStatus(value).value
  87. except ValueError as exc:
  88. raise InvalidArgumentError("status必须是ENABLED或DISABLED") from exc
  89. def _boolean(value: Any, *, field: str) -> bool:
  90. if type(value) is not bool:
  91. raise InvalidArgumentError(f"{field}必须是布尔值")
  92. return value
  93. def _row_version(value: Any) -> int:
  94. if type(value) is not int or value < 0:
  95. raise InvalidArgumentError("rowVersion必须是非负整数")
  96. return value
  97. def _id_list(value: Any, *, field: str) -> list[int]:
  98. if not isinstance(value, list) or any(
  99. not isinstance(item, str) or not item.isdecimal() or int(item) <= 0
  100. for item in value
  101. ):
  102. raise InvalidArgumentError(f"{field}必须是正整数形式的字符串ID数组")
  103. ids = [int(item) for item in value]
  104. if len(set(ids)) != len(ids):
  105. raise InvalidArgumentError(f"{field}不能包含重复ID")
  106. return ids
  107. def _exact_payload(payload: Any, expected: set[str]) -> dict[str, Any]:
  108. if not isinstance(payload, dict) or set(payload) != expected:
  109. raise InvalidArgumentError(
  110. "请求体字段必须且只能包含:" + "、".join(sorted(expected))
  111. )
  112. return payload
  113. def _role_row(session, role_id: int, *, for_update: bool = False) -> Role:
  114. stmt = select(Role).where(Role.id == role_id, Role.is_deleted.is_(False))
  115. if for_update:
  116. stmt = stmt.with_for_update()
  117. role = session.scalar(stmt)
  118. if role is None:
  119. raise ResourceNotFoundError("角色不存在")
  120. return role
  121. def _member_counts(session, role_ids: list[int]) -> dict[int, int]:
  122. if not role_ids:
  123. return {}
  124. rows = session.execute(
  125. select(User.role_id, func.count(User.id))
  126. .where(User.role_id.in_(role_ids), User.is_deleted.is_(False))
  127. .group_by(User.role_id)
  128. ).all()
  129. return {role_id: count for role_id, count in rows}
  130. def role_summary(role: Role, member_count: int) -> dict[str, object]:
  131. return {
  132. "id": serialize_id(role.id),
  133. "roleName": role.role_name,
  134. "sortNo": role.sort_no,
  135. "status": role.status,
  136. "remark": role.remark,
  137. "isDefault": role.is_default,
  138. "memberCount": member_count,
  139. "rowVersion": role.row_version,
  140. }
  141. def list_roles(
  142. *,
  143. keyword: str | None,
  144. status: str | None,
  145. page_request: PageRequest,
  146. ) -> dict[str, object]:
  147. filters = [Role.is_deleted.is_(False)]
  148. if keyword:
  149. filters.append(Role.role_name.like(f"%{keyword.strip()}%"))
  150. if status:
  151. try:
  152. normalized_status = EnabledStatus(status).value
  153. except ValueError as exc:
  154. raise InvalidArgumentError("status必须是ENABLED或DISABLED") from exc
  155. filters.append(Role.status == normalized_status)
  156. total = db.session.scalar(select(func.count(Role.id)).where(*filters)) or 0
  157. roles = db.session.scalars(
  158. select(Role)
  159. .where(*filters)
  160. .order_by(Role.sort_no.asc(), Role.id.asc())
  161. .offset(page_request.offset)
  162. .limit(page_request.page_size)
  163. ).all()
  164. counts = _member_counts(db.session, [role.id for role in roles])
  165. return page_result(
  166. [role_summary(role, counts.get(role.id, 0)) for role in roles],
  167. page=page_request.page,
  168. page_size=page_request.page_size,
  169. total=total,
  170. )
  171. def _clear_other_defaults(session, *, keep_id: int | None) -> None:
  172. """将除 keep_id 外的活跃默认角色取消默认标记(先清后设避免唯一约束冲突)。"""
  173. stmt = select(Role).where(
  174. Role.is_default.is_(True),
  175. Role.is_deleted.is_(False),
  176. )
  177. if keep_id is not None:
  178. stmt = stmt.where(Role.id != keep_id)
  179. actor_id = get_auth_context().user_id
  180. now = _utc_now()
  181. for existing in session.scalars(stmt).all():
  182. existing.is_default = False
  183. existing.updated_by = actor_id
  184. existing.updated_at = now
  185. existing.row_version += 1
  186. def _ensure_role_name_free(session, role_name: str, *, exclude_id: int) -> None:
  187. duplicated = session.scalar(
  188. select(Role.id).where(
  189. Role.role_name == role_name,
  190. Role.is_deleted.is_(False),
  191. Role.id != exclude_id,
  192. )
  193. )
  194. if duplicated is not None:
  195. raise ConflictError("角色名已存在")
  196. def _seed_permission_rows(session, role_id: int, actor_id: int | None) -> None:
  197. for level in SecurityLevel:
  198. for scope in DeptScope:
  199. session.add(
  200. RolePermission(
  201. role_id=role_id,
  202. security_level=level.value,
  203. dept_scope=scope.value,
  204. created_by=actor_id,
  205. updated_by=actor_id,
  206. )
  207. )
  208. def create_role(payload: Any) -> dict[str, object]:
  209. body = _exact_payload(payload, ROLE_CREATE_FIELDS)
  210. role_name = _required_text(body["roleName"], field="roleName", max_length=64)
  211. sort_no = _sort_no(body["sortNo"])
  212. status = _status_value(body["status"])
  213. remark = _optional_text(body["remark"], field="remark", max_length=255)
  214. is_default = _boolean(body["isDefault"], field="isDefault")
  215. actor_id = get_auth_context().user_id
  216. try:
  217. with transaction() as session:
  218. _ensure_role_name_free(session, role_name, exclude_id=0)
  219. if is_default:
  220. _clear_other_defaults(session, keep_id=None)
  221. session.flush()
  222. role = Role(
  223. role_name=role_name,
  224. sort_no=sort_no,
  225. status=status,
  226. remark=remark,
  227. is_default=is_default,
  228. created_by=actor_id,
  229. updated_by=actor_id,
  230. )
  231. session.add(role)
  232. session.flush()
  233. _seed_permission_rows(session, role.id, actor_id)
  234. session.add(
  235. business_audit(
  236. action=AuditAction.CREATE_ROLE,
  237. target=AuditTarget.ROLE,
  238. target_id=role.id,
  239. target_name=role.role_name,
  240. detail={
  241. "roleName": role.role_name,
  242. "sortNo": role.sort_no,
  243. "status": role.status,
  244. "remark": role.remark,
  245. "isDefault": role.is_default,
  246. },
  247. )
  248. )
  249. return role_summary(role, 0)
  250. except IntegrityError as exc:
  251. raise ConflictError("角色名已存在或默认角色设置冲突") from exc
  252. def update_role(role_id: int, payload: Any) -> dict[str, object]:
  253. body = _exact_payload(payload, ROLE_UPDATE_FIELDS)
  254. role_name = _required_text(body["roleName"], field="roleName", max_length=64)
  255. sort_no = _sort_no(body["sortNo"])
  256. status = _status_value(body["status"])
  257. remark = _optional_text(body["remark"], field="remark", max_length=255)
  258. is_default = _boolean(body["isDefault"], field="isDefault")
  259. expected_version = _row_version(body["rowVersion"])
  260. actor_id = get_auth_context().user_id
  261. now = _utc_now()
  262. with transaction() as session:
  263. role = _role_row(session, role_id, for_update=True)
  264. if role.row_version != expected_version:
  265. raise ConflictError(
  266. "数据已被其他用户修改,请刷新后重试",
  267. details={"currentRowVersion": role.row_version},
  268. )
  269. _ensure_role_name_free(session, role_name, exclude_id=role.id)
  270. before = {
  271. "roleName": role.role_name,
  272. "sortNo": role.sort_no,
  273. "status": role.status,
  274. "remark": role.remark,
  275. "isDefault": role.is_default,
  276. }
  277. # 新设默认:先清旧默认,避免 default_marker 唯一约束冲突
  278. if is_default and not role.is_default:
  279. _clear_other_defaults(session, keep_id=role.id)
  280. session.flush()
  281. role.role_name = role_name
  282. role.sort_no = sort_no
  283. role.status = status
  284. role.remark = remark
  285. role.is_default = is_default
  286. role.updated_by = actor_id
  287. role.updated_at = now
  288. role.row_version += 1
  289. after = {
  290. "roleName": role.role_name,
  291. "sortNo": role.sort_no,
  292. "status": role.status,
  293. "remark": role.remark,
  294. "isDefault": role.is_default,
  295. }
  296. session.add(
  297. business_audit(
  298. action=AuditAction.EDIT_ROLE,
  299. target=AuditTarget.ROLE,
  300. target_id=role.id,
  301. target_name=role.role_name,
  302. detail={"before": before, "after": after},
  303. )
  304. )
  305. member_count = _member_counts(db.session, [role.id]).get(role.id, 0)
  306. return role_summary(role, member_count)
  307. def delete_role(role_id: int, row_version: int) -> dict[str, object]:
  308. actor_id = get_auth_context().user_id
  309. now = _utc_now()
  310. with transaction() as session:
  311. role = _role_row(session, role_id, for_update=True)
  312. if role.row_version != row_version:
  313. raise ConflictError(
  314. "数据已被其他用户修改,请刷新后重试",
  315. details={"currentRowVersion": role.row_version},
  316. )
  317. bound_users = (
  318. session.scalar(
  319. select(func.count(User.id)).where(
  320. User.role_id == role_id,
  321. User.is_deleted.is_(False),
  322. )
  323. )
  324. or 0
  325. )
  326. if bound_users:
  327. raise RoleInUseError(
  328. details={"memberCount": bound_users},
  329. )
  330. target_name = role.role_name
  331. role.is_deleted = True
  332. role.deleted_at = now
  333. role.updated_by = actor_id
  334. role.updated_at = now
  335. role.row_version += 1
  336. # 部门配置与矩阵行随角色软删,保留审计追溯
  337. for model in (RoleDepartment, RolePermission):
  338. for row in session.scalars(
  339. select(model).where(
  340. model.role_id == role_id,
  341. model.is_deleted.is_(False),
  342. )
  343. ).all():
  344. row.is_deleted = True
  345. row.deleted_at = now
  346. row.updated_by = actor_id
  347. row.updated_at = now
  348. session.add(
  349. business_audit(
  350. action=AuditAction.DELETE_ROLE,
  351. target=AuditTarget.ROLE,
  352. target_id=role_id,
  353. target_name=target_name,
  354. detail={"roleName": target_name, "rowVersion": role.row_version},
  355. )
  356. )
  357. return {"id": serialize_id(role_id), "deleted": True}
  358. def list_role_departments(role_id: int) -> list[str]:
  359. _role_row(db.session, role_id)
  360. organization_ids = db.session.scalars(
  361. select(RoleDepartment.organization_id)
  362. .where(
  363. RoleDepartment.role_id == role_id,
  364. RoleDepartment.is_deleted.is_(False),
  365. )
  366. .order_by(RoleDepartment.organization_id.asc())
  367. ).all()
  368. return [serialize_id(value) for value in organization_ids]
  369. def update_role_departments(role_id: int, payload: Any) -> dict[str, object]:
  370. body = _exact_payload(payload, ROLE_DEPARTMENT_FIELDS)
  371. organization_ids = _id_list(body["organizationIds"], field="organizationIds")
  372. expected_version = _row_version(body["rowVersion"])
  373. actor_id = get_auth_context().user_id
  374. now = _utc_now()
  375. with transaction() as session:
  376. role = _role_row(session, role_id, for_update=True)
  377. if role.row_version != expected_version:
  378. raise ConflictError(
  379. "数据已被其他用户修改,请刷新后重试",
  380. details={"currentRowVersion": role.row_version},
  381. )
  382. target_ids = set(organization_ids)
  383. if target_ids:
  384. valid_ids = set(
  385. session.scalars(
  386. select(Organization.id).where(
  387. Organization.id.in_(target_ids),
  388. Organization.is_deleted.is_(False),
  389. )
  390. ).all()
  391. )
  392. if target_ids - valid_ids:
  393. raise InvalidArgumentError("organizationIds包含无效机构")
  394. existing_rows = session.scalars(
  395. select(RoleDepartment).where(
  396. RoleDepartment.role_id == role_id,
  397. RoleDepartment.is_deleted.is_(False),
  398. )
  399. ).all()
  400. existing_map = {row.organization_id: row for row in existing_rows}
  401. removed, added = [], []
  402. for org_id, row in existing_map.items():
  403. if org_id not in target_ids:
  404. row.is_deleted = True
  405. row.deleted_at = now
  406. row.updated_by = actor_id
  407. row.updated_at = now
  408. removed.append(org_id)
  409. for org_id in sorted(target_ids - existing_map.keys()):
  410. session.add(
  411. RoleDepartment(
  412. role_id=role_id,
  413. organization_id=org_id,
  414. created_by=actor_id,
  415. updated_by=actor_id,
  416. )
  417. )
  418. added.append(org_id)
  419. role.updated_by = actor_id
  420. role.updated_at = now
  421. role.row_version += 1
  422. session.add(
  423. business_audit(
  424. action=AuditAction.CHANGE_ROLE_PERMISSION,
  425. target=AuditTarget.ROLE,
  426. target_id=role_id,
  427. target_name=role.role_name,
  428. detail={
  429. "scope": "departments",
  430. "addedOrganizationIds": [serialize_id(v) for v in added],
  431. "removedOrganizationIds": [serialize_id(v) for v in removed],
  432. },
  433. )
  434. )
  435. return {
  436. "id": serialize_id(role_id),
  437. "organizationIds": [serialize_id(v) for v in sorted(target_ids)],
  438. "rowVersion": role.row_version,
  439. }
  440. def _permission_row_dto(row: RolePermission) -> dict[str, object]:
  441. dto: dict[str, object] = {
  442. "securityLevel": row.security_level,
  443. "deptScope": row.dept_scope,
  444. }
  445. for name, attr in PERMISSION_FIELDS:
  446. dto[name] = getattr(row, attr)
  447. return dto
  448. def list_role_permissions(role_id: int) -> list[dict[str, object]]:
  449. _role_row(db.session, role_id)
  450. rows = db.session.scalars(
  451. select(RolePermission)
  452. .where(
  453. RolePermission.role_id == role_id,
  454. RolePermission.is_deleted.is_(False),
  455. )
  456. .order_by(RolePermission.id.asc())
  457. ).all()
  458. return [_permission_row_dto(row) for row in rows]
  459. def _validate_matrix(value: Any) -> list[dict[str, Any]]:
  460. expected_row_fields = {"securityLevel", "deptScope"} | {
  461. name for name, _ in PERMISSION_FIELDS
  462. }
  463. if not isinstance(value, list) or len(value) != len(MATRIX_KEYS):
  464. raise InvalidArgumentError(f"权限矩阵必须为{len(MATRIX_KEYS)}行")
  465. rows: dict[tuple[str, str], dict[str, Any]] = {}
  466. for item in value:
  467. if not isinstance(item, dict) or set(item) != expected_row_fields:
  468. raise InvalidArgumentError(
  469. "矩阵行字段必须且只能包含:" + "、".join(sorted(expected_row_fields))
  470. )
  471. key = (item["securityLevel"], item["deptScope"])
  472. if key not in MATRIX_KEYS:
  473. raise InvalidArgumentError("矩阵行的密级或部门范围不是有效枚举值")
  474. if key in rows:
  475. raise InvalidArgumentError("矩阵行不能重复")
  476. for name, _ in PERMISSION_FIELDS:
  477. if type(item[name]) is not bool:
  478. raise InvalidArgumentError(f"{name}必须是布尔值")
  479. rows[key] = item
  480. return list(value)
  481. def update_role_permissions(role_id: int, payload: Any) -> dict[str, object]:
  482. body = _exact_payload(payload, ROLE_PERMISSION_FIELDS)
  483. matrix = _validate_matrix(body["matrix"])
  484. expected_version = _row_version(body["rowVersion"])
  485. actor_id = get_auth_context().user_id
  486. now = _utc_now()
  487. with transaction() as session:
  488. role = _role_row(session, role_id, for_update=True)
  489. if role.row_version != expected_version:
  490. raise ConflictError(
  491. "数据已被其他用户修改,请刷新后重试",
  492. details={"currentRowVersion": role.row_version},
  493. )
  494. existing_rows = session.scalars(
  495. select(RolePermission).where(
  496. RolePermission.role_id == role_id,
  497. RolePermission.is_deleted.is_(False),
  498. )
  499. ).all()
  500. by_key = {(row.security_level, row.dept_scope): row for row in existing_rows}
  501. if set(by_key) != MATRIX_KEYS:
  502. raise ConflictError("角色权限矩阵数据不完整,请联系管理员核查")
  503. changed_keys: list[dict[str, object]] = []
  504. for item in matrix:
  505. row = by_key[(item["securityLevel"], item["deptScope"])]
  506. before_flags = {
  507. name: getattr(row, attr) for name, attr in PERMISSION_FIELDS
  508. }
  509. after_flags = {name: item[name] for name, _ in PERMISSION_FIELDS}
  510. if before_flags != after_flags:
  511. row.updated_by = actor_id
  512. row.updated_at = now
  513. for name, attr in PERMISSION_FIELDS:
  514. setattr(row, attr, item[name])
  515. changed_keys.append(
  516. {
  517. "securityLevel": row.security_level,
  518. "deptScope": row.dept_scope,
  519. "before": before_flags,
  520. "after": after_flags,
  521. }
  522. )
  523. role.updated_by = actor_id
  524. role.updated_at = now
  525. role.row_version += 1
  526. session.add(
  527. business_audit(
  528. action=AuditAction.CHANGE_ROLE_PERMISSION,
  529. target=AuditTarget.ROLE,
  530. target_id=role_id,
  531. target_name=role.role_name,
  532. detail={"scope": "matrix", "changedRows": changed_keys},
  533. )
  534. )
  535. return {
  536. "id": serialize_id(role_id),
  537. "matrix": list_role_permissions(role_id),
  538. "rowVersion": role.row_version,
  539. }
  540. def list_role_users(role_id: int) -> dict[str, object]:
  541. _role_row(db.session, role_id)
  542. user_ids = db.session.scalars(
  543. select(User.id)
  544. .where(User.role_id == role_id, User.is_deleted.is_(False))
  545. .order_by(User.id.asc())
  546. ).all()
  547. return {"id": serialize_id(role_id), "userIds": [serialize_id(v) for v in user_ids]}
  548. def update_role_users(role_id: int, payload: Any) -> dict[str, object]:
  549. body = _exact_payload(payload, ROLE_USER_FIELDS)
  550. user_ids = _id_list(body["userIds"], field="userIds")
  551. expected_version = _row_version(body["rowVersion"])
  552. actor_id = get_auth_context().user_id
  553. now = _utc_now()
  554. with transaction() as session:
  555. role = _role_row(session, role_id, for_update=True)
  556. if role.row_version != expected_version:
  557. raise ConflictError(
  558. "数据已被其他用户修改,请刷新后重试",
  559. details={"currentRowVersion": role.row_version},
  560. )
  561. target_ids = set(user_ids)
  562. found: dict[int, User] = {}
  563. if target_ids:
  564. for user in session.scalars(
  565. select(User)
  566. .where(User.id.in_(target_ids), User.is_deleted.is_(False))
  567. .with_for_update()
  568. ).all():
  569. found[user.id] = user
  570. if set(found) != target_ids:
  571. raise InvalidArgumentError("userIds包含无效用户")
  572. for user in found.values():
  573. if user.role_code != RoleCode.USER.value:
  574. raise InvalidArgumentError("仅普通用户可关联自定义角色")
  575. current_users = session.scalars(
  576. select(User)
  577. .where(User.role_id == role_id, User.is_deleted.is_(False))
  578. .with_for_update()
  579. ).all()
  580. added_ids: list[int] = []
  581. removed_ids: list[int] = []
  582. for user in current_users:
  583. if user.id not in target_ids:
  584. user.role_id = None
  585. user.auth_version += 1
  586. user.updated_by = actor_id
  587. user.updated_at = now
  588. user.row_version += 1
  589. removed_ids.append(user.id)
  590. for user_id in sorted(target_ids):
  591. user = found[user_id]
  592. if user.role_id != role_id:
  593. user.role_id = role_id
  594. user.auth_version += 1
  595. user.updated_by = actor_id
  596. user.updated_at = now
  597. user.row_version += 1
  598. added_ids.append(user_id)
  599. role.updated_by = actor_id
  600. role.updated_at = now
  601. role.row_version += 1
  602. session.add(
  603. business_audit(
  604. action=AuditAction.CHANGE_ROLE_USER,
  605. target=AuditTarget.ROLE,
  606. target_id=role_id,
  607. target_name=role.role_name,
  608. detail={
  609. "addedUserIds": [serialize_id(v) for v in added_ids],
  610. "removedUserIds": [serialize_id(v) for v in removed_ids],
  611. },
  612. )
  613. )
  614. return {
  615. "id": serialize_id(role_id),
  616. "userIds": [serialize_id(v) for v in sorted(target_ids)],
  617. "rowVersion": role.row_version,
  618. }