role_service.py 23 KB

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