test_governance_audit_api.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. from __future__ import annotations
  2. class FakeGovernanceAuditService:
  3. def __init__(self):
  4. self.calls = []
  5. def coverage(self, **filters):
  6. self.calls.append(("coverage", filters))
  7. return {
  8. "period_start": filters["period_start"].isoformat(),
  9. "period_end": filters["period_end"].isoformat(),
  10. "categories": [
  11. {
  12. "category": "authentication",
  13. "count": 1,
  14. "available": True,
  15. "latest_at": "2026-07-30T00:00:00Z",
  16. }
  17. ],
  18. }
  19. def list_events(self, **filters):
  20. self.calls.append(("events", filters))
  21. return {
  22. "records": [
  23. {
  24. "event_uid": "authentication:1",
  25. "category": "authentication",
  26. "action": "login",
  27. "status": "success",
  28. "safe_detail": {"username": "admin"},
  29. }
  30. ],
  31. "page": int(filters["page"]),
  32. "page_size": int(filters["page_size"]),
  33. "total": 1,
  34. }
  35. def create_seal(self, **payload):
  36. self.calls.append(("create_seal", payload))
  37. return {
  38. "uid": "seal-1",
  39. "event_count": 1,
  40. "root_hash": "a" * 64,
  41. "signature": "b" * 64,
  42. }
  43. def list_seals(self, *, limit):
  44. self.calls.append(("list_seals", {"limit": limit}))
  45. return [{"uid": "seal-1", "event_count": 1}]
  46. def verify_seal(self, seal_uid):
  47. self.calls.append(("verify_seal", {"seal_uid": seal_uid}))
  48. return {"uid": seal_uid, "integrity_status": "intact"}
  49. def _client(monkeypatch, *, role="admin"):
  50. from app import create_app
  51. from app.api.system import governance_audit
  52. service = FakeGovernanceAuditService()
  53. identity = {
  54. "id": "00000000-0000-7000-8000-000000000111",
  55. "roles": [role],
  56. }
  57. monkeypatch.setattr(
  58. "app.core.system.permissions.authenticate_request",
  59. lambda: identity,
  60. )
  61. monkeypatch.setattr(
  62. governance_audit,
  63. "get_governance_audit_service",
  64. lambda: service,
  65. )
  66. monkeypatch.setattr(
  67. governance_audit,
  68. "get_security_checks",
  69. lambda: {
  70. "seal_ready": True,
  71. "assurance": "签名封存用于检测篡改,不等于阻止数据库管理员修改",
  72. "checks": [{"code": "safe_error_boundary", "status": "passed"}],
  73. },
  74. )
  75. app = create_app()
  76. app.config.update(TESTING=True)
  77. return app.test_client(), service
  78. def test_admin_reads_security_coverage_events_and_seals(monkeypatch):
  79. http, service = _client(monkeypatch)
  80. window = (
  81. "period_start=2026-07-01T00:00:00Z&"
  82. "period_end=2026-07-30T00:00:00Z"
  83. )
  84. security = http.get(
  85. "/api/system/governance-audit/security-checks"
  86. )
  87. coverage = http.get(
  88. f"/api/system/governance-audit/coverage?{window}"
  89. )
  90. events = http.get(
  91. f"/api/system/governance-audit/events?{window}"
  92. "&category=authentication&page=1&page_size=20"
  93. )
  94. seals = http.get("/api/system/governance-audit/seals?limit=25")
  95. assert security.status_code == 200
  96. assert security.get_json()["data"]["seal_ready"] is True
  97. assert coverage.status_code == 200
  98. assert events.status_code == 200
  99. assert events.get_json()["data"]["records"][0]["safe_detail"] == {
  100. "username": "admin"
  101. }
  102. assert seals.status_code == 200
  103. assert [call[0] for call in service.calls] == [
  104. "coverage",
  105. "events",
  106. "list_seals",
  107. ]
  108. def test_seal_creation_uses_server_identity_and_verification_is_explicit(
  109. monkeypatch,
  110. ):
  111. http, service = _client(monkeypatch)
  112. created = http.post(
  113. "/api/system/governance-audit/seals",
  114. json={
  115. "period_start": "2026-07-01T00:00:00Z",
  116. "period_end": "2026-07-30T00:00:00Z",
  117. "categories": ["authentication", "ingestion"],
  118. "sealed_by": "attacker-controlled",
  119. },
  120. )
  121. verified = http.post(
  122. "/api/system/governance-audit/seals/seal-1/verify"
  123. )
  124. assert created.status_code == 200
  125. assert service.calls[0][1]["actor_uid"] == (
  126. "00000000-0000-7000-8000-000000000111"
  127. )
  128. assert "sealed_by" not in service.calls[0][1]
  129. assert verified.status_code == 200
  130. assert verified.get_json()["data"]["integrity_status"] == "intact"
  131. def test_viewer_and_editor_cannot_read_or_seal(monkeypatch):
  132. for role in ("viewer", "editor"):
  133. http, _service = _client(monkeypatch, role=role)
  134. assert http.get(
  135. "/api/system/governance-audit/events"
  136. ).status_code == 403
  137. assert http.post(
  138. "/api/system/governance-audit/seals",
  139. json={},
  140. ).status_code == 403
  141. def test_invalid_query_and_repository_failure_return_safe_contract(monkeypatch):
  142. from app.core.system.governance_audit import GovernanceAuditInvalid
  143. http, service = _client(monkeypatch)
  144. def invalid(**_kwargs):
  145. raise GovernanceAuditInvalid("page_size must be between 1 and 100")
  146. service.list_events = invalid
  147. response = http.get(
  148. "/api/system/governance-audit/events"
  149. "?period_start=2026-07-01T00:00:00Z"
  150. "&period_end=2026-07-30T00:00:00Z&page_size=101"
  151. )
  152. assert response.status_code == 400
  153. assert response.get_json()["error"]["code"] == (
  154. "GOVERNANCE_AUDIT_INVALID"
  155. )
  156. def unavailable(**_kwargs):
  157. raise RuntimeError(
  158. "postgresql://admin:database-secret@db/dataops "
  159. "authorization=Bearer-secret"
  160. )
  161. service.list_events = unavailable
  162. failed = http.get(
  163. "/api/system/governance-audit/events"
  164. "?period_start=2026-07-01T00:00:00Z"
  165. "&period_end=2026-07-30T00:00:00Z"
  166. )
  167. payload = failed.get_json()
  168. assert failed.status_code == 503
  169. assert payload["message"] == "审计与运行证据暂不可用"
  170. assert "database-secret" not in repr(payload)
  171. assert "Bearer-secret" not in repr(payload)
  172. def test_production_without_dedicated_key_blocks_seal_and_verify(monkeypatch):
  173. from flask import Flask
  174. from app.api.system import governance_audit
  175. production = Flask(__name__)
  176. production.config.update(
  177. FLASK_ENV="production",
  178. TESTING=False,
  179. )
  180. development = Flask(__name__)
  181. development.config.update(
  182. FLASK_ENV="development",
  183. TESTING=False,
  184. )
  185. with production.app_context():
  186. assert governance_audit._sealing_allowed(False) is False
  187. assert governance_audit._sealing_allowed(True) is True
  188. with development.app_context():
  189. assert governance_audit._sealing_allowed(False) is True
  190. http, service = _client(monkeypatch)
  191. monkeypatch.setattr(
  192. governance_audit,
  193. "_sealing_allowed",
  194. lambda _dedicated_ready: False,
  195. )
  196. created = http.post(
  197. "/api/system/governance-audit/seals",
  198. json={
  199. "period_start": "2026-07-01T00:00:00Z",
  200. "period_end": "2026-07-30T00:00:00Z",
  201. },
  202. )
  203. verified = http.post(
  204. "/api/system/governance-audit/seals/seal-1/verify"
  205. )
  206. assert created.status_code == 503
  207. assert verified.status_code == 503
  208. assert service.calls == []