trusted_delivery.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. """No-store HTTP boundary for closed trusted-delivery contracts."""
  2. from datetime import UTC, datetime
  3. from flask import g, jsonify, request
  4. from sqlalchemy.exc import DBAPIError, IntegrityError
  5. from app import db
  6. from app.api.system import bp
  7. from app.core.system.permissions import (
  8. SECURITY_GOVERNANCE_MANAGE,
  9. SECURITY_GOVERNANCE_OPERATE,
  10. require_permissions,
  11. )
  12. from app.core.system.trusted_delivery import TrustedDeliveryService
  13. from app.core.system.trusted_delivery_repository import (
  14. SqlAlchemyTrustedDeliveryRepository,
  15. )
  16. from app.core.system.trusted_delivery_subscription_repository import (
  17. SqlAlchemyTrustedDeliverySubscriptionRepository,
  18. )
  19. from app.core.system.trusted_delivery_subscriptions import (
  20. TrustedDeliverySubscriptionService,
  21. )
  22. from app.models.result import failed, success
  23. MAX_TRUSTED_DELIVERY_REQUEST_BYTES = 16 * 1024
  24. def _service():
  25. return TrustedDeliveryService(SqlAlchemyTrustedDeliveryRepository(db.session))
  26. def _subscription_service():
  27. return TrustedDeliverySubscriptionService(
  28. SqlAlchemyTrustedDeliverySubscriptionRepository(db.session)
  29. )
  30. def _execute(operation, *, created=False):
  31. try:
  32. value = operation()
  33. # Service/repository methods deliberately leave transaction ownership to
  34. # the HTTP boundary. Commit before returning success so a second
  35. # connection can observe a successful response immediately.
  36. db.session.commit()
  37. response = jsonify(success(value, code=201 if created else 200))
  38. response.headers["Cache-Control"] = "no-store"
  39. return (response, 201) if created else response
  40. except PermissionError:
  41. db.session.rollback()
  42. response = jsonify(failed("trusted delivery request denied", code=403))
  43. except (IntegrityError, DBAPIError):
  44. db.session.rollback()
  45. response = jsonify(failed("trusted delivery request conflict", code=409))
  46. except (LookupError, RuntimeError, ValueError, TypeError):
  47. db.session.rollback()
  48. response = jsonify(failed("trusted delivery request rejected", code=400))
  49. response.headers["Cache-Control"] = "no-store"
  50. return response, response.get_json()["code"]
  51. def _body(allowed):
  52. if request.content_length is not None and request.content_length > MAX_TRUSTED_DELIVERY_REQUEST_BYTES:
  53. raise ValueError("trusted delivery request is too large")
  54. if len(request.get_data(cache=True)) > MAX_TRUSTED_DELIVERY_REQUEST_BYTES:
  55. raise ValueError("trusted delivery request is too large")
  56. body = request.get_json(silent=True)
  57. if not isinstance(body, dict) or set(body) - allowed:
  58. raise ValueError("closed request schema")
  59. return body
  60. @bp.post("/trusted-delivery/policy-versions")
  61. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  62. def create_trusted_delivery_policy():
  63. return _execute(lambda: _service().create_policy_version(_body({"code", "version", "selector", "resource_rules"}), actor_uid=g.current_user["id"]), created=True)
  64. @bp.post("/trusted-delivery/policy-versions/activate")
  65. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  66. def activate_trusted_delivery_policy():
  67. return _execute(lambda: _service().activate_policy_version(_body({"code", "version", "approval_ref", "approval_digest", "idempotency_key"}), actor_uid=g.current_user["id"]))
  68. @bp.post("/trusted-delivery/policy-versions/rollback")
  69. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  70. def rollback_trusted_delivery_policy():
  71. return _execute(lambda: _service().rollback_policy_version(_body({"code", "version", "approval_ref", "approval_digest", "idempotency_key"}), actor_uid=g.current_user["id"]))
  72. @bp.post("/trusted-delivery/decisions")
  73. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  74. def evaluate_trusted_delivery():
  75. allowed = {"subject_uid", "roles", "business_domain_uid", "asset_uid", "purpose", "environment", "action", "classification", "requested_fields"}
  76. body = _body(allowed)
  77. body["subject_uid"] = g.current_user["id"]
  78. body["roles"] = list(g.current_user["roles"])
  79. return _execute(lambda: _service().evaluate_gateway(body))
  80. @bp.post("/trusted-delivery/provisions")
  81. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  82. def provision_trusted_delivery():
  83. allowed = {"policy_uid", "approval_ref", "approval_digest", "asset_uid", "business_domain_uid", "purpose", "environment", "action", "classification", "requested_fields", "expires_at", "target_capability", "provider", "idempotency_key"}
  84. def operation():
  85. body = _body(allowed)
  86. # Browser callers never choose the identity facts used for policy matching.
  87. body["subject_uid"] = g.current_user["id"]
  88. return _service().provision(body, actor_uid=g.current_user["id"], subject_roles=list(g.current_user["roles"]))
  89. return _execute(operation, created=True)
  90. @bp.post("/trusted-delivery/legal-holds")
  91. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  92. def create_trusted_delivery_hold():
  93. return _execute(lambda: _service().create_legal_hold(_body({"asset_uid", "operation", "approver_refs", "evidence_digest"}), actor_uid=g.current_user["id"]), created=True)
  94. @bp.post("/trusted-delivery/legal-holds/release")
  95. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  96. def release_trusted_delivery_hold():
  97. return _execute(lambda: _service().release_legal_hold(_body({"hold_uid", "approval_ref", "approval_digest", "idempotency_key"}), actor_uid=g.current_user["id"]))
  98. @bp.post("/trusted-delivery/reclaim/preview")
  99. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  100. def trusted_delivery_reclaim_preview():
  101. body = _body({"as_of"})
  102. as_of = body.get("as_of") or datetime.now(UTC).isoformat()
  103. return _execute(lambda: {"candidates": _service().preview_reclaim(as_of=as_of, actor_uid=g.current_user["id"]), "executed": False})
  104. @bp.post("/trusted-delivery/reclaim/execute")
  105. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  106. def trusted_delivery_reclaim_execute():
  107. return _execute(lambda: _service().execute_reclaim(_body({"grant_uid", "idempotency_key"}), actor_uid=g.current_user["id"]))
  108. @bp.post("/trusted-delivery/subscriptions")
  109. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  110. def create_trusted_delivery_subscription():
  111. allowed = {"grant_uid", "asset_uid", "trigger", "purpose", "expires_at", "idempotency_key"}
  112. return _execute(lambda: _subscription_service().create_subscription(_body(allowed), actor_uid=g.current_user["id"]), created=True)
  113. @bp.post("/trusted-delivery/subscriptions/<subscription_uid>/pause")
  114. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  115. def pause_trusted_delivery_subscription(subscription_uid):
  116. return _execute(lambda: _subscription_service().pause_subscription(subscription_uid, actor_uid=g.current_user["id"]))
  117. @bp.post("/trusted-delivery/subscriptions/<subscription_uid>/activate")
  118. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  119. def activate_trusted_delivery_subscription(subscription_uid):
  120. return _execute(lambda: _subscription_service().activate_subscription(subscription_uid, actor_uid=g.current_user["id"]))
  121. @bp.post("/trusted-delivery/subscriptions/<subscription_uid>/resume")
  122. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  123. def resume_trusted_delivery_subscription(subscription_uid):
  124. return _execute(lambda: _subscription_service().resume_subscription(subscription_uid, actor_uid=g.current_user["id"]))
  125. @bp.post("/trusted-delivery/subscriptions/<subscription_uid>/terminate")
  126. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  127. def terminate_trusted_delivery_subscription(subscription_uid):
  128. return _execute(lambda: _subscription_service().terminate_subscription(subscription_uid, actor_uid=g.current_user["id"]))
  129. @bp.post("/trusted-delivery/subscriptions/deliveries/<delivery_uid>/compensate")
  130. @require_permissions(SECURITY_GOVERNANCE_MANAGE)
  131. def compensate_trusted_delivery_subscription_delivery(delivery_uid):
  132. return _execute(lambda: _subscription_service().compensate_dead_letter(
  133. delivery_uid, **_body({"reason_code", "receipt_code"}), actor_uid=g.current_user["id"]
  134. ))
  135. @bp.post("/trusted-delivery/subscriptions/anomalies")
  136. @require_permissions(SECURITY_GOVERNANCE_OPERATE)
  137. def report_trusted_delivery_subscription_anomaly():
  138. allowed = {"kind", "asset_uid", "subscription_uid", "incident_uid", "summary", "correlation_id"}
  139. return _execute(lambda: _subscription_service().report_anomaly(_body(allowed), actor_uid=g.current_user["id"]), created=True)