test_trusted_delivery.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. from __future__ import annotations
  2. from copy import deepcopy
  3. from datetime import UTC, datetime, timedelta
  4. import pytest
  5. OWNER = "01900000-0000-7000-8000-000000060001"
  6. APPROVER = "01900000-0000-7000-8000-000000060002"
  7. USER = "01900000-0000-7000-8000-000000060003"
  8. DOMAIN = "01900000-0000-7000-8000-000000060101"
  9. ASSET = "01900000-0000-7000-8000-000000060201"
  10. class MemoryTrustedDeliveryRepository:
  11. def __init__(self):
  12. self.users = {OWNER, APPROVER, USER}
  13. self.policies = {}
  14. self.grants = {}
  15. self.holds = {}
  16. self.events = []
  17. def users_available(self, values):
  18. return set(values) & self.users
  19. def create_policy_version(self, record):
  20. self.policies[record["uid"]] = deepcopy(record)
  21. return deepcopy(record)
  22. def active_policies(self):
  23. return [deepcopy(value) for value in self.policies.values() if value["status"] == "active"]
  24. def create_grant(self, record):
  25. self.grants[record["uid"]] = deepcopy(record)
  26. return deepcopy(record)
  27. def grants_due_for_revoke(self, instant):
  28. return [deepcopy(value) for value in self.grants.values() if value["status"] == "active"]
  29. def active_hold_for_asset(self, asset_uid):
  30. return next((deepcopy(value) for value in self.holds.values() if value["asset_uid"] == asset_uid and value["status"] == "active"), None)
  31. def update_grant_status(self, uid, expected_version, status, reason_code):
  32. saved = deepcopy(self.grants[uid])
  33. if saved["current_version"] != expected_version:
  34. raise RuntimeError("trusted delivery version conflict")
  35. saved.update(status=status, reason_code=reason_code, current_version=expected_version + 1)
  36. self.grants[uid] = saved
  37. return deepcopy(saved)
  38. def create_legal_hold(self, record):
  39. self.holds[record["uid"]] = deepcopy(record)
  40. return deepcopy(record)
  41. def add_evidence(self, record):
  42. self.events.append(deepcopy(record))
  43. class FakeProvider:
  44. def __init__(self):
  45. self.calls = []
  46. def apply(self, envelope):
  47. self.calls.append(deepcopy(envelope))
  48. return {"status": "applied", "receipt_code": "fake-accepted", "response_digest": "a" * 64}
  49. @pytest.fixture()
  50. def trusted():
  51. from app.core.system.trusted_delivery import (
  52. ClosedProviderRegistry,
  53. TrustedDeliveryService,
  54. )
  55. now = datetime(2026, 8, 11, 8, 0, tzinfo=UTC)
  56. ids = iter(f"01900000-0000-7000-8000-{value:012d}" for value in range(6300, 6500))
  57. provider = FakeProvider()
  58. registry = ClosedProviderRegistry.for_tests({"database": provider})
  59. repository = MemoryTrustedDeliveryRepository()
  60. return (
  61. TrustedDeliveryService(
  62. repository, provider_registry=registry, uid_factory=lambda: next(ids), now_factory=lambda: now
  63. ),
  64. repository,
  65. provider,
  66. now,
  67. )
  68. def _policy_payload(now):
  69. return {
  70. "code": "MATERIAL_TEST_READ",
  71. "version": "1.0.0",
  72. "selector": {
  73. "subjects": [USER], "roles": ["editor"], "business_domains": [DOMAIN],
  74. "assets": [ASSET], "purposes": ["quality_review"], "environments": ["test"],
  75. "actions": ["read"], "classifications": ["sensitive"],
  76. "expires_at": (now + timedelta(days=3)).isoformat(),
  77. },
  78. "resource_rules": {
  79. "row_filter": {"op": "eq", "field": "region_code", "value": "CN-31"},
  80. "field_rules": [
  81. {"field": "material_code", "effect": "allow", "mask_ref": None},
  82. {"field": "supplier_phone", "effect": "allow", "mask_ref": "display-phone-v1"},
  83. ],
  84. },
  85. }
  86. def test_gateway_matches_structured_policy_and_returns_safe_projection(trusted):
  87. service, _repository, _provider, now = trusted
  88. policy = service.create_policy_version(_policy_payload(now), actor_uid=OWNER)
  89. service.activate_policy_version({"code": policy["code"], "version": policy["version"], "approval_ref": "approval:gateway", "approval_digest": "a" * 64, "idempotency_key": "gateway-activate"}, actor_uid=APPROVER)
  90. decision = service.evaluate_gateway({
  91. "subject_uid": USER, "roles": ["editor"], "business_domain_uid": DOMAIN,
  92. "asset_uid": ASSET, "purpose": "quality_review", "environment": "test",
  93. "action": "read", "classification": "sensitive",
  94. "requested_fields": ["material_code", "supplier_phone"],
  95. })
  96. assert decision == {
  97. "decision": "allow", "reason_code": "policy_allowed",
  98. "policy_code": "MATERIAL_TEST_READ", "policy_version": "1.0.0",
  99. "projected_fields": ["material_code", "supplier_phone"],
  100. "row_filter": {"op": "eq", "field": "region_code", "value": "CN-31"},
  101. "field_masks": {"supplier_phone": "display-phone-v1"},
  102. }
  103. def test_gateway_rejects_unsafe_dsl_and_high_sensitivity_export(trusted):
  104. service, _repository, _provider, now = trusted
  105. malformed = _policy_payload(now)
  106. malformed["resource_rules"]["row_filter"] = {"op": "sql", "value": "select 1"}
  107. with pytest.raises(ValueError, match="unsupported row filter"):
  108. service.create_policy_version(malformed, actor_uid=OWNER)
  109. service.create_policy_version(_policy_payload(now), actor_uid=OWNER)
  110. denied = service.evaluate_gateway({
  111. "subject_uid": USER, "roles": ["editor"], "business_domain_uid": DOMAIN,
  112. "asset_uid": ASSET, "purpose": "quality_review", "environment": "test",
  113. "action": "export", "classification": "highly_sensitive", "requested_fields": ["material_code"],
  114. })
  115. assert denied == {"decision": "deny", "reason_code": "highly_sensitive_export_denied"}
  116. def test_grant_is_approval_bound_provider_disabled_fails_closed_and_expiry_recovery_obeys_hold(trusted):
  117. service, repository, provider, now = trusted
  118. policy = service.create_policy_version(_policy_payload(now), actor_uid=OWNER)
  119. service.activate_policy_version({"code": policy["code"], "version": policy["version"], "approval_ref": "approval:grant", "approval_digest": "a" * 64, "idempotency_key": "grant-activate"}, actor_uid=APPROVER)
  120. decision = service.evaluate_gateway({
  121. "subject_uid": USER, "roles": ["editor"], "business_domain_uid": DOMAIN,
  122. "asset_uid": ASSET, "purpose": "quality_review", "environment": "test",
  123. "action": "read", "classification": "sensitive", "requested_fields": ["material_code"],
  124. })
  125. grant = service.create_grant({
  126. "policy_uid": policy["uid"], "approval_ref": "approval:wp06-test:1",
  127. "approval_digest": "b" * 64, "subject_uid": USER, "asset_uid": ASSET,
  128. "provider": "database", "expires_at": (now + timedelta(minutes=1)).isoformat(),
  129. "decision": decision,
  130. }, actor_uid=OWNER)
  131. receipt = service.dispatch_grant(grant["uid"], actor_uid=OWNER)
  132. assert receipt["status"] == "applied"
  133. assert provider.calls[0]["grant_uid"] == grant["uid"]
  134. assert "approval_ref" not in provider.calls[0]
  135. assert service.provider_registry.is_enterprise_ready is False
  136. with pytest.raises(PermissionError, match="disabled"):
  137. service.create_grant({
  138. "policy_uid": policy["uid"], "approval_ref": "approval:wp06-test:2", "approval_digest": "c" * 64,
  139. "subject_uid": USER, "asset_uid": ASSET, "provider": "iam",
  140. "expires_at": (now + timedelta(minutes=1)).isoformat(), "decision": decision,
  141. }, actor_uid=OWNER)
  142. hold = service.create_legal_hold({
  143. "asset_uid": ASSET, "operation": "freeze", "approver_refs": ["approval:one", "approval:two"],
  144. "evidence_digest": "d" * 64,
  145. }, actor_uid=OWNER)
  146. assert hold["status"] == "active"
  147. assert service.revoke_expired(as_of=now + timedelta(minutes=2), actor_uid=OWNER) == []
  148. assert repository.grants[grant["uid"]]["status"] == "active"
  149. def test_mask_preview_is_deterministic_and_never_echoes_raw_input(trusted):
  150. service, _repository, _provider, _now = trusted
  151. first = service.mask_preview({"mode": "display", "mask_ref": "display-phone-v1", "value": "13800138000"})
  152. second = service.mask_preview({"mode": "display", "mask_ref": "display-phone-v1", "value": "13800138000"})
  153. assert first == second
  154. assert "13800138000" not in repr(first)
  155. assert first["masked_value"] == "***-****"
  156. def test_lifecycle_methods_are_available_for_versioned_policy_and_reclaim(trusted):
  157. """P3-WP06 B1 closes the durable lifecycle rather than only previewing it."""
  158. service, _repository, _provider, _now = trusted
  159. assert callable(service.activate_policy_version)
  160. assert callable(service.rollback_policy_version)
  161. assert callable(service.release_legal_hold)
  162. assert callable(service.execute_reclaim)
  163. def test_policy_activation_and_hold_release_require_independent_replay_safe_approval(trusted):
  164. service, repository, _provider, now = trusted
  165. first = service.create_policy_version(_policy_payload(now), actor_uid=OWNER)
  166. second_payload = _policy_payload(now)
  167. second_payload["version"] = "2.0.0"
  168. second = service.create_policy_version(second_payload, actor_uid=OWNER)
  169. assert second["status"] == "draft"
  170. request = {
  171. "code": first["code"], "version": "2.0.0", "approval_ref": "approval:activate:2",
  172. "approval_digest": "1" * 64, "idempotency_key": "policy-activate-2",
  173. }
  174. activated = service.activate_policy_version(request, actor_uid=APPROVER)
  175. assert activated["status"] == "active"
  176. assert repository.policies[first["uid"]]["status"] == "draft"
  177. assert service.activate_policy_version(request, actor_uid=APPROVER) == activated
  178. with pytest.raises(RuntimeError, match="idempotency conflict"):
  179. service.activate_policy_version({**request, "version": "1.0.0"}, actor_uid=APPROVER)
  180. hold = service.create_legal_hold({
  181. "asset_uid": ASSET, "operation": "freeze", "approver_refs": ["approval:hold:one", "approval:hold:two"],
  182. "evidence_digest": "2" * 64,
  183. }, actor_uid=OWNER)
  184. release = {"hold_uid": hold["uid"], "approval_ref": "approval:release:one", "approval_digest": "3" * 64, "idempotency_key": "hold-release-1"}
  185. with pytest.raises(PermissionError, match="independent"):
  186. service.release_legal_hold(release, actor_uid=OWNER)
  187. released = service.release_legal_hold(release, actor_uid=APPROVER)
  188. assert released["status"] == "released"
  189. assert service.release_legal_hold(release, actor_uid=APPROVER) == released
  190. def test_active_hold_denies_export_gateway_decision(trusted):
  191. service, _repository, _provider, now = trusted
  192. payload = _policy_payload(now)
  193. payload["selector"]["actions"] = ["export"]
  194. policy = service.create_policy_version(payload, actor_uid=OWNER)
  195. service.activate_policy_version({"code": policy["code"], "version": policy["version"], "approval_ref": "approval:export", "approval_digest": "a" * 64, "idempotency_key": "export-activate"}, actor_uid=APPROVER)
  196. service.create_legal_hold({
  197. "asset_uid": ASSET, "operation": "export_review", "approver_refs": ["approval:export:one", "approval:export:two"],
  198. "evidence_digest": "4" * 64,
  199. }, actor_uid=OWNER)
  200. assert service.evaluate_gateway({
  201. "subject_uid": USER, "roles": ["editor"], "business_domain_uid": DOMAIN,
  202. "asset_uid": ASSET, "purpose": "quality_review", "environment": "test",
  203. "action": "export", "classification": "sensitive", "requested_fields": ["material_code"],
  204. }) == {"decision": "deny", "reason_code": "active_legal_hold"}
  205. def test_closed_contract_rejects_bare_secret_material_without_echo(trusted):
  206. service, _repository, _provider, _now = trusted
  207. with pytest.raises(ValueError, match="unsafe"):
  208. service.mask_preview({"mode": "display", "mask_ref": "display-phone-v1", "value": "password"})