tenant_resources.py 1.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. """Tenant namespace and hash-only manifest contracts for non-PostgreSQL stores."""
  2. from __future__ import annotations
  3. import re
  4. from typing import Any
  5. from app.core.system.tenant_context import TenantScope
  6. class TenantResourceError(ValueError):
  7. pass
  8. _RESOURCE_TYPES = frozenset(
  9. {
  10. "object",
  11. "graph",
  12. "cache",
  13. "index",
  14. "keys",
  15. "connectors",
  16. "models",
  17. "plugins",
  18. "backup",
  19. "audit_export",
  20. }
  21. )
  22. _RESOURCE_NAME = re.compile(r"^[a-z0-9][a-z0-9._-]{0,120}$")
  23. _DIGEST = re.compile(r"^[0-9a-f]{64}$")
  24. def _resource_token(value: str, *, allowed: frozenset[str] | None = None) -> str:
  25. if not isinstance(value, str) or not _RESOURCE_NAME.fullmatch(value):
  26. raise TenantResourceError("tenant_resource_invalid")
  27. if allowed is not None and value not in allowed:
  28. raise TenantResourceError("tenant_resource_invalid")
  29. return value
  30. def build_tenant_namespace(scope: TenantScope, resource: str, name: str) -> str:
  31. """Return an opaque prefix; callers must not concatenate their own paths."""
  32. resource = _resource_token(resource, allowed=_RESOURCE_TYPES)
  33. name = _resource_token(name)
  34. return f"{scope.tenant_id}/{resource}/v{scope.version}/{name}"
  35. def build_tenant_resource_manifest(
  36. scope: TenantScope,
  37. *,
  38. resource: str,
  39. name: str,
  40. digest: str,
  41. ) -> dict[str, Any]:
  42. if not isinstance(digest, str) or not _DIGEST.fullmatch(digest):
  43. raise TenantResourceError("tenant_resource_invalid")
  44. namespace = build_tenant_namespace(scope, resource, name)
  45. return {
  46. "schema_version": 1,
  47. "tenant_id": scope.tenant_id,
  48. "delivery_mode": scope.mode.value,
  49. "resource": resource,
  50. "namespace": namespace,
  51. "digest": f"sha256:{digest}",
  52. "provider_enabled": False,
  53. }