audit_service.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """认证审计日志构造与独立失败事务。"""
  2. from __future__ import annotations
  3. from typing import Any
  4. from flask import request
  5. from dms.common.enums import AuditAction, AuditResult, AuditTarget
  6. from dms.common.request_context import get_request_id
  7. from dms.extensions import db
  8. from dms.models import AuditLog, User
  9. from dms.security.auth_context import get_auth_context
  10. def client_ip() -> str | None:
  11. forwarded = request.headers.get("X-Forwarded-For", "")
  12. value = forwarded.split(",", 1)[0].strip() if forwarded else request.remote_addr
  13. return value[:64] if value else None
  14. def auth_audit(
  15. *,
  16. action: AuditAction,
  17. result: AuditResult,
  18. user: User | None = None,
  19. attempted_username: str | None = None,
  20. failure_reason: str | None = None,
  21. detail: dict[str, Any] | None = None,
  22. ) -> AuditLog:
  23. username = user.username if user is not None else attempted_username
  24. return AuditLog(
  25. user_id=user.id if user is not None else None,
  26. username=username,
  27. real_name=user.real_name if user is not None else None,
  28. organization_id=user.organization_id if user is not None else None,
  29. organization_name=user.organization_name if user is not None else None,
  30. action_type=action.value,
  31. target_type=AuditTarget.AUTH.value,
  32. target_id=user.id if user is not None else None,
  33. target_name=username,
  34. operation_result=result.value,
  35. failure_reason=failure_reason,
  36. client_ip=client_ip(),
  37. user_agent=(request.user_agent.string or "")[:500] or None,
  38. request_id=get_request_id(),
  39. operation_detail=detail,
  40. )
  41. def business_audit(
  42. *,
  43. action: AuditAction,
  44. target: AuditTarget,
  45. target_id: int,
  46. target_name: str,
  47. detail: dict[str, Any] | None = None,
  48. ) -> AuditLog:
  49. """使用请求级认证快照构造修改类业务成功审计。"""
  50. context = get_auth_context()
  51. return AuditLog(
  52. user_id=context.user_id,
  53. username=context.username,
  54. real_name=context.real_name,
  55. organization_id=context.organization_id,
  56. organization_name=context.organization_name,
  57. action_type=action.value,
  58. target_type=target.value,
  59. target_id=target_id,
  60. target_name=target_name,
  61. operation_result=AuditResult.SUCCESS.value,
  62. failure_reason=None,
  63. client_ip=client_ip(),
  64. user_agent=(request.user_agent.string or "")[:500] or None,
  65. request_id=get_request_id(),
  66. operation_detail=detail,
  67. )
  68. def record_login_failure(
  69. *,
  70. user: User | None,
  71. attempted_username: str,
  72. failure_reason: str,
  73. ) -> None:
  74. """在登录主流程之外用短事务持久化失败审计。"""
  75. db.session.rollback()
  76. try:
  77. db.session.add(
  78. auth_audit(
  79. action=AuditAction.LOGIN,
  80. result=AuditResult.FAILURE,
  81. user=user,
  82. attempted_username=attempted_username,
  83. failure_reason=failure_reason,
  84. detail={"username": attempted_username},
  85. )
  86. )
  87. db.session.commit()
  88. except Exception:
  89. db.session.rollback()
  90. raise