responsibilities.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. from __future__ import annotations
  2. from flask import g, jsonify, request
  3. from app import db
  4. from app.api.system import bp
  5. from app.core.governance.responsibilities import (
  6. ResponsibilityConflict,
  7. ResponsibilityService,
  8. ResponsibilityUserUnavailable,
  9. ResponsibilityValidationError,
  10. SqlAlchemyResponsibilityRepository,
  11. )
  12. from app.core.governance.unified_responsibilities import (
  13. UnifiedResponsibilityService,
  14. )
  15. from app.core.governance.unified_responsibility_repository import (
  16. SqlAlchemyUnifiedResponsibilityRepository,
  17. )
  18. from app.core.system.permissions import (
  19. RESPONSIBILITIES_MANAGE,
  20. RESPONSIBILITIES_READ,
  21. require_permissions,
  22. )
  23. from app.models.result import failed, success
  24. def _service() -> ResponsibilityService:
  25. return ResponsibilityService(SqlAlchemyResponsibilityRepository(db.session))
  26. def _unified_service() -> UnifiedResponsibilityService:
  27. return UnifiedResponsibilityService(
  28. SqlAlchemyUnifiedResponsibilityRepository(db.session),
  29. commit=db.session.commit,
  30. rollback=db.session.rollback,
  31. )
  32. def _etag(response, revision: int):
  33. response.headers["ETag"] = f'"{int(revision)}"'
  34. return response
  35. def _expected_revision() -> int:
  36. raw = str(request.headers.get("If-Match") or "").strip()
  37. if raw.startswith("W/"):
  38. raw = raw[2:].strip()
  39. raw = raw.strip('"')
  40. if not raw or not raw.isdigit():
  41. raise ResponsibilityValidationError("missing valid If-Match revision")
  42. return int(raw)
  43. def _unified_error(exc: Exception):
  44. db.session.rollback()
  45. message = str(exc)
  46. if "If-Match" in message:
  47. status = 428
  48. elif isinstance(exc, LookupError):
  49. status = 404
  50. elif isinstance(exc, RuntimeError):
  51. status = 409
  52. else:
  53. status = 400
  54. return jsonify(failed(message, code=status)), status
  55. @bp.route(
  56. "/responsibilities/<resource_type>/<resource_uid>",
  57. methods=["GET"],
  58. )
  59. @require_permissions(RESPONSIBILITIES_READ)
  60. def get_responsibility_matrix(resource_type: str, resource_uid: str):
  61. try:
  62. result = _service().get(resource_type, resource_uid)
  63. return _etag(jsonify(success(result)), result["revision"])
  64. except ResponsibilityValidationError as exc:
  65. return jsonify(failed(str(exc), code=400)), 400
  66. @bp.route(
  67. "/responsibilities/<resource_type>/<resource_uid>",
  68. methods=["PUT"],
  69. )
  70. @require_permissions(RESPONSIBILITIES_MANAGE)
  71. def replace_responsibility_matrix(resource_type: str, resource_uid: str):
  72. try:
  73. revision = _expected_revision()
  74. body = request.get_json(silent=True) or {}
  75. result = _service().replace(
  76. resource_type=resource_type,
  77. resource_uid=resource_uid,
  78. assignments=body.get("assignments"),
  79. expected_revision=revision,
  80. actor_uid=g.current_user["id"],
  81. )
  82. db.session.commit()
  83. return _etag(jsonify(success(result, "责任矩阵已更新")), result["revision"])
  84. except ResponsibilityConflict as exc:
  85. db.session.rollback()
  86. return jsonify(failed(str(exc), code=409)), 409
  87. except ResponsibilityUserUnavailable as exc:
  88. db.session.rollback()
  89. return jsonify(failed(str(exc), code=422)), 422
  90. except ResponsibilityValidationError as exc:
  91. db.session.rollback()
  92. status = 428 if "If-Match" in str(exc) else 400
  93. return jsonify(failed(str(exc), code=status)), status
  94. @bp.route(
  95. "/responsibilities/<resource_type>/<resource_uid>/resolved",
  96. methods=["GET"],
  97. )
  98. @require_permissions(RESPONSIBILITIES_READ)
  99. def resolve_responsibility(resource_type: str, resource_uid: str):
  100. try:
  101. return jsonify(
  102. success(
  103. _unified_service().resolve(
  104. resource_type,
  105. resource_uid,
  106. at=request.args.get("at"),
  107. )
  108. )
  109. )
  110. except (ValueError, LookupError, RuntimeError) as exc:
  111. return _unified_error(exc)
  112. @bp.route(
  113. "/responsibilities/hierarchy/<resource_type>/<resource_uid>",
  114. methods=["PUT"],
  115. )
  116. @require_permissions(RESPONSIBILITIES_MANAGE)
  117. def replace_responsibility_parent(resource_type: str, resource_uid: str):
  118. try:
  119. body = request.get_json(silent=True) or {}
  120. result = _unified_service().set_parent(
  121. {
  122. **body,
  123. "resource_type": resource_type,
  124. "resource_uid": resource_uid,
  125. },
  126. expected_revision=_expected_revision(),
  127. actor_uid=g.current_user["id"],
  128. )
  129. return _etag(
  130. jsonify(success(result, "责任继承关系已更新")), result["revision"]
  131. )
  132. except (ValueError, LookupError, RuntimeError, ResponsibilityValidationError) as exc:
  133. return _unified_error(exc)
  134. @bp.route("/responsibilities/delegations", methods=["GET"])
  135. @require_permissions(RESPONSIBILITIES_READ)
  136. def list_responsibility_delegations():
  137. try:
  138. return jsonify(
  139. success(
  140. _unified_service().list_delegations(
  141. status=request.args.get("status")
  142. )
  143. )
  144. )
  145. except (ValueError, RuntimeError) as exc:
  146. return _unified_error(exc)
  147. @bp.route("/responsibilities/delegations", methods=["POST"])
  148. @require_permissions(RESPONSIBILITIES_MANAGE)
  149. def create_responsibility_delegation():
  150. try:
  151. result = _unified_service().create_delegation(
  152. request.get_json(silent=True) or {},
  153. actor_uid=g.current_user["id"],
  154. )
  155. return jsonify(success(result, "责任委派已创建", code=201)), 201
  156. except (ValueError, RuntimeError) as exc:
  157. return _unified_error(exc)
  158. @bp.route(
  159. "/responsibilities/delegations/<delegation_uid>/revoke",
  160. methods=["POST"],
  161. )
  162. @require_permissions(RESPONSIBILITIES_MANAGE)
  163. def revoke_responsibility_delegation(delegation_uid: str):
  164. try:
  165. result = _unified_service().revoke_delegation(
  166. delegation_uid,
  167. expected_version=_expected_revision(),
  168. actor_uid=g.current_user["id"],
  169. )
  170. return _etag(
  171. jsonify(success(result, "责任委派已收回")), result["current_version"]
  172. )
  173. except (ValueError, LookupError, RuntimeError, ResponsibilityValidationError) as exc:
  174. return _unified_error(exc)
  175. @bp.route("/responsibilities/delegations/expire", methods=["POST"])
  176. @require_permissions(RESPONSIBILITIES_MANAGE)
  177. def expire_responsibility_delegations():
  178. try:
  179. body = request.get_json(silent=True) or {}
  180. return jsonify(
  181. success(
  182. _unified_service().expire_delegations(
  183. at=body.get("at"), actor_uid=g.current_user["id"]
  184. ),
  185. "到期委派已收回",
  186. )
  187. )
  188. except (ValueError, RuntimeError) as exc:
  189. return _unified_error(exc)
  190. @bp.route("/responsibilities/transfers", methods=["POST"])
  191. @require_permissions(RESPONSIBILITIES_MANAGE)
  192. def transfer_departing_responsibility():
  193. try:
  194. result = _unified_service().transfer_departing_user(
  195. request.get_json(silent=True) or {},
  196. actor_uid=g.current_user["id"],
  197. )
  198. return jsonify(success(result, "离岗责任已转交", code=201)), 201
  199. except (ValueError, RuntimeError) as exc:
  200. return _unified_error(exc)
  201. @bp.route("/responsibilities/policies", methods=["GET"])
  202. @require_permissions(RESPONSIBILITIES_READ)
  203. def list_responsibility_policies():
  204. try:
  205. return jsonify(success(_unified_service().list_policies()))
  206. except RuntimeError as exc:
  207. return _unified_error(exc)
  208. @bp.route("/responsibilities/policies", methods=["POST"])
  209. @require_permissions(RESPONSIBILITIES_MANAGE)
  210. def create_responsibility_policy():
  211. try:
  212. result = _unified_service().create_policy(
  213. request.get_json(silent=True) or {},
  214. actor_uid=g.current_user["id"],
  215. )
  216. return _etag(
  217. jsonify(success(result, "责任策略草稿已创建", code=201)),
  218. result["current_version"],
  219. ), 201
  220. except (ValueError, RuntimeError) as exc:
  221. return _unified_error(exc)
  222. @bp.route(
  223. "/responsibilities/policies/<policy_uid>/revisions",
  224. methods=["POST"],
  225. )
  226. @require_permissions(RESPONSIBILITIES_MANAGE)
  227. def revise_responsibility_policy(policy_uid: str):
  228. try:
  229. body = request.get_json(silent=True) or {}
  230. result = _unified_service().revise_policy(
  231. policy_uid,
  232. body.get("definition"),
  233. expected_version=_expected_revision(),
  234. actor_uid=g.current_user["id"],
  235. )
  236. return _etag(
  237. jsonify(success(result, "责任策略新版本已创建")),
  238. result["current_version"],
  239. )
  240. except (ValueError, LookupError, RuntimeError, ResponsibilityValidationError) as exc:
  241. return _unified_error(exc)
  242. @bp.route(
  243. "/responsibilities/policies/<policy_uid>/publish",
  244. methods=["POST"],
  245. )
  246. @require_permissions(RESPONSIBILITIES_MANAGE)
  247. def publish_responsibility_policy(policy_uid: str):
  248. try:
  249. result = _unified_service().publish_policy(
  250. policy_uid,
  251. expected_version=_expected_revision(),
  252. actor_uid=g.current_user["id"],
  253. )
  254. return _etag(
  255. jsonify(success(result, "责任策略已发布")),
  256. result["current_version"],
  257. )
  258. except (ValueError, LookupError, RuntimeError, ResponsibilityValidationError) as exc:
  259. return _unified_error(exc)
  260. @bp.route(
  261. "/responsibilities/<resource_type>/<resource_uid>/joint-review/evaluate",
  262. methods=["POST"],
  263. )
  264. @require_permissions(RESPONSIBILITIES_MANAGE)
  265. def evaluate_responsibility_joint_review(resource_type: str, resource_uid: str):
  266. try:
  267. body = request.get_json(silent=True) or {}
  268. return jsonify(
  269. success(
  270. _unified_service().evaluate_joint_review(
  271. resource_type, resource_uid, body.get("decisions")
  272. )
  273. )
  274. )
  275. except (ValueError, LookupError, RuntimeError) as exc:
  276. return _unified_error(exc)
  277. @bp.route("/responsibilities/operations", methods=["GET"])
  278. @require_permissions(RESPONSIBILITIES_READ)
  279. def responsibility_operations():
  280. try:
  281. return jsonify(
  282. success(
  283. _unified_service().operations(request.args.get("owner_uid"))
  284. )
  285. )
  286. except (ValueError, RuntimeError) as exc:
  287. return _unified_error(exc)