0009_reorder_audit_action.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """方案拖动排序:审计动作CHECK扩展。
  2. Revision ID: 0009_reorder_audit_action
  3. Revises: 0008_document_sort_no
  4. Create Date: 2026-08-18
  5. 变更要点:
  6. 1. sys_audit_log 的 action_type CHECK 约束扩展,覆盖拖动排序新增的
  7. REORDER_DOCUMENT / REORDER_ATTACHMENT 两个审计动作。
  8. 2. 降级保护:已产生排序审计记录时拒绝有损降级。
  9. """
  10. from __future__ import annotations
  11. from collections.abc import Sequence
  12. from alembic import op
  13. import sqlalchemy as sa
  14. revision: str = "0009_reorder_audit_action"
  15. down_revision: str | None = "0008_document_sort_no"
  16. branch_labels: str | Sequence[str] | None = None
  17. depends_on: str | Sequence[str] | None = None
  18. # 既有审计动作(与 0004 对齐)+ 本期新增动作
  19. AUDIT_ACTIONS_V3 = (
  20. "LOGIN",
  21. "LOGOUT",
  22. "VIEW_DOCUMENT",
  23. "DOWNLOAD_DOCUMENT",
  24. "UPLOAD_DOCUMENT",
  25. "BATCH_IMPORT",
  26. "EDIT_DOCUMENT",
  27. "DELETE_DOCUMENT",
  28. "CREATE_CATEGORY",
  29. "EDIT_CATEGORY",
  30. "DELETE_CATEGORY",
  31. "CHANGE_PERMISSION",
  32. "BIND_ATTACHMENT",
  33. "UNBIND_ATTACHMENT",
  34. "RESTORE_DOCUMENT",
  35. "CREATE_ORGANIZATION",
  36. "EDIT_ORGANIZATION",
  37. "DELETE_ORGANIZATION",
  38. "CREATE_USER",
  39. "EDIT_USER",
  40. "DELETE_USER",
  41. "RESET_USER_PASSWORD",
  42. "REORDER_DOCUMENT",
  43. "REORDER_ATTACHMENT",
  44. )
  45. AUDIT_ACTIONS_V2 = AUDIT_ACTIONS_V3[:22]
  46. ACTION_CONSTRAINT = "audit_action_type"
  47. def _check_sql(values: tuple[str, ...]) -> str:
  48. # 安全性:values 为本文件内硬编码的枚举常量,非用户输入,无注入风险
  49. inline = ", ".join(f"'{v}'" for v in values)
  50. return f"action_type IN ({inline})"
  51. def _existing_constraint_names(table: str) -> set[str]:
  52. inspector = sa.inspect(op.get_bind())
  53. return {item["name"] for item in inspector.get_check_constraints(table)}
  54. def _drop_action_check(existing: set[str]) -> None:
  55. # op.drop_constraint 会按 metadata.naming_convention 再次前缀化约束名
  56. # ("ck_%(table_name)s_%(constraint_name)s"),因此必须传入原始约束名,
  57. # 让 SQLAlchemy 渲染出最终名;数据库实际存的也是渲染后的最终名。
  58. final_name = f"ck_sys_audit_log_{ACTION_CONSTRAINT}"
  59. if final_name in existing:
  60. op.drop_constraint(ACTION_CONSTRAINT, "sys_audit_log", type_="check")
  61. def upgrade() -> None:
  62. _drop_action_check(_existing_constraint_names("sys_audit_log"))
  63. op.create_check_constraint(
  64. ACTION_CONSTRAINT,
  65. "sys_audit_log",
  66. _check_sql(AUDIT_ACTIONS_V3),
  67. )
  68. def downgrade() -> None:
  69. bind = op.get_bind()
  70. reorder_rows = bind.scalar(
  71. sa.text(
  72. "SELECT COUNT(*) FROM sys_audit_log "
  73. "WHERE action_type IN ('REORDER_DOCUMENT','REORDER_ATTACHMENT')"
  74. )
  75. )
  76. if reorder_rows:
  77. raise RuntimeError(
  78. "0009已产生拖动排序相关审计记录,拒绝有损降级;"
  79. "请先备份并按数据治理流程处理"
  80. )
  81. _drop_action_check(_existing_constraint_names("sys_audit_log"))
  82. op.create_check_constraint(
  83. ACTION_CONSTRAINT,
  84. "sys_audit_log",
  85. _check_sql(AUDIT_ACTIONS_V2),
  86. )