tenant_context.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. """Closed, server-derived tenant scope for the conditional WP10 foundation."""
  2. from __future__ import annotations
  3. import re
  4. from collections.abc import Mapping
  5. from dataclasses import dataclass
  6. from enum import StrEnum
  7. class TenantContextError(ValueError):
  8. """A tenant boundary was absent, unapproved, or contradicted by input."""
  9. class DeliveryMode(StrEnum):
  10. PRIVATE_SINGLE_TENANT = "private_single_tenant"
  11. SHARED_CONTROL_PLANE = "shared_control_plane"
  12. SAAS = "saas"
  13. _TENANT_ID = re.compile(r"^[a-z][a-z0-9-]{0,62}$")
  14. _HOST = re.compile(r"^[a-z0-9][a-z0-9.-]{0,252}$")
  15. _EVENT_TYPE = re.compile(r"^[a-z][a-z0-9_.-]{0,80}$")
  16. _DIGEST = re.compile(r"^[0-9a-f]{64}$")
  17. def _tenant_id(value: str) -> str:
  18. if not isinstance(value, str) or not _TENANT_ID.fullmatch(value):
  19. raise TenantContextError("tenant_scope_denied")
  20. return value
  21. @dataclass(frozen=True)
  22. class TenantScope:
  23. tenant_id: str
  24. mode: DeliveryMode
  25. version: int = 1
  26. def __post_init__(self) -> None:
  27. _tenant_id(self.tenant_id)
  28. if self.version != 1:
  29. raise TenantContextError("tenant_context_version_denied")
  30. def require_background_scope(self, declared_tenant_id: str | None) -> str:
  31. if declared_tenant_id is None:
  32. raise TenantContextError("tenant_scope_required")
  33. if _tenant_id(declared_tenant_id) != self.tenant_id:
  34. raise TenantContextError("tenant_scope_denied")
  35. return self.tenant_id
  36. def require_admin_scope(self, declared_tenant_id: str | None) -> str:
  37. return self.require_background_scope(declared_tenant_id)
  38. TenantContext = TenantScope
  39. def validate_tenant_event_envelope(scope: TenantScope, envelope: Mapping[str, object]) -> dict[str, object]:
  40. """Reject task/event dispatch unless its scope is explicit and server-matched."""
  41. required = {"schema_version", "tenant_id", "event_type", "payload_digest"}
  42. if set(envelope) - required:
  43. raise TenantContextError("tenant_event_closed")
  44. tenant_id = envelope.get("tenant_id")
  45. if tenant_id is None:
  46. raise TenantContextError("tenant_scope_required")
  47. if set(envelope) != required:
  48. raise TenantContextError("tenant_event_closed")
  49. if not isinstance(tenant_id, str) or _tenant_id(tenant_id) != scope.tenant_id:
  50. raise TenantContextError("tenant_scope_denied")
  51. if envelope.get("schema_version") != 1 or not isinstance(envelope.get("event_type"), str) or not _EVENT_TYPE.fullmatch(envelope["event_type"]):
  52. raise TenantContextError("tenant_event_closed")
  53. if not isinstance(envelope.get("payload_digest"), str) or not _DIGEST.fullmatch(envelope["payload_digest"]):
  54. raise TenantContextError("tenant_event_closed")
  55. return dict(envelope)
  56. class TenantContextResolver:
  57. """Resolve scope only from server-held delivery configuration and host mapping."""
  58. def __init__(
  59. self,
  60. *,
  61. mode: DeliveryMode,
  62. approved_default_tenant: str | None,
  63. route_tenants: Mapping[str, str],
  64. activated_modes: frozenset[DeliveryMode] | None = None,
  65. ) -> None:
  66. self.mode = DeliveryMode(mode)
  67. self.approved_default_tenant = (
  68. _tenant_id(approved_default_tenant)
  69. if approved_default_tenant is not None
  70. else None
  71. )
  72. self.route_tenants = {
  73. self._host(host): _tenant_id(tenant_id)
  74. for host, tenant_id in route_tenants.items()
  75. }
  76. self.activated_modes = activated_modes or frozenset(
  77. {DeliveryMode.PRIVATE_SINGLE_TENANT}
  78. )
  79. @staticmethod
  80. def _host(host: str) -> str:
  81. if not isinstance(host, str) or not _HOST.fullmatch(host.lower()):
  82. raise TenantContextError("tenant_route_denied")
  83. return host.lower()
  84. def resolve_request(
  85. self,
  86. *,
  87. host: str,
  88. claimed_tenant_id: str | None,
  89. principal_id: str,
  90. ) -> TenantContext:
  91. if not isinstance(principal_id, str) or not principal_id:
  92. raise TenantContextError("tenant_principal_required")
  93. if self.mode not in self.activated_modes:
  94. raise TenantContextError("delivery_mode_not_approved")
  95. self._host(host)
  96. if self.mode is DeliveryMode.PRIVATE_SINGLE_TENANT:
  97. if self.approved_default_tenant is None:
  98. raise TenantContextError("tenant_default_not_approved")
  99. # Client claim is intentionally ignored: it never selects a tenant.
  100. return TenantContext(self.approved_default_tenant, self.mode)
  101. resolved = self.route_tenants.get(self._host(host))
  102. if resolved is None:
  103. raise TenantContextError("tenant_route_denied")
  104. if claimed_tenant_id is not None and _tenant_id(claimed_tenant_id) != resolved:
  105. raise TenantContextError("tenant_spoof_denied")
  106. return TenantContext(resolved, self.mode)