Files
ITWorx_MCP_Hub-Public/services/runtime/common.py
T

148 lines
5.3 KiB
Python

from __future__ import annotations
import hashlib
import hmac
import json
import os
import re
import time
from collections.abc import Mapping
from pathlib import Path
from typing import Any
from urllib.parse import quote_plus
from uuid import NAMESPACE_URL, UUID, uuid5
from packages.connector_kit import ConnectorContext
from packages.hub_runtime import read_secret_file
ENVIRONMENTS = frozenset({"dev", "test", "staging", "prod"})
PROJECT_SLUG = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$")
IDENTITY_ASSERTION = re.compile(r"^v1:([0-9]{10}):([0-9a-f]{64})$")
TRACE_ID = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
def deterministic_id(kind: str, value: str) -> UUID:
return uuid5(NAMESPACE_URL, f"https://hub.example.com/{kind}/{value}")
def load_connector_identity_key() -> bytes:
path = Path(
os.environ.get("CONNECTOR_IDENTITY_KEY_FILE", "/run/secrets/connector_identity_signing_key")
)
key = read_secret_file(path).encode("utf-8")
if len(key) < 32:
raise ValueError("connector identity signing key must contain at least 32 bytes")
return key
def identity_assertion(
key: bytes,
*,
project: str,
environment: str,
client_id: str,
trace_id: str,
now: int | None = None,
) -> str:
timestamp = int(time.time()) if now is None else now
message = f"v1\n{timestamp}\n{project}\n{environment}\n{client_id}\n{trace_id}".encode()
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
return f"v1:{timestamp}:{signature}"
def connector_context(
headers: Mapping[str, str],
projects: set[str],
*,
signing_key: bytes,
now: int | None = None,
) -> ConnectorContext:
project = headers.get("x-itworx-project", "").strip().lower()
environment = headers.get("x-itworx-environment", "").strip().lower()
client_id = headers.get("x-itworx-client-id", "").strip().lower()
trace_id = headers.get("x-itworx-trace-id", "").strip()
if project not in projects or not PROJECT_SLUG.fullmatch(project):
raise PermissionError("connector project context is not registered")
if environment not in ENVIRONMENTS:
raise PermissionError("connector environment context is not registered")
try:
UUID(client_id)
except ValueError as exc:
raise PermissionError("connector client context is invalid") from exc
assertion = headers.get("x-itworx-identity-assertion", "").strip().lower()
match = IDENTITY_ASSERTION.fullmatch(assertion)
reference = int(time.time()) if now is None else now
if match is None or abs(reference - int(match.group(1))) > 60:
raise PermissionError("connector identity assertion is invalid or expired")
expected = identity_assertion(
signing_key,
project=project,
environment=environment,
client_id=client_id,
trace_id=trace_id,
now=int(match.group(1)),
)
if not hmac.compare_digest(assertion, expected):
raise PermissionError("connector identity assertion is invalid or expired")
return ConnectorContext(
workspace_id=deterministic_id("workspace", "itworx"),
project_id=deterministic_id("project", project),
project_slug=project,
environment=environment,
client_id=UUID(client_id),
# The trace id is already covered by the identity assertion above, so it is
# authenticated Hub input; the pattern check only keeps it a safe header value.
correlation_id=trace_id if TRACE_ID.fullmatch(trace_id) else None,
)
def request_headers(context: Any) -> Mapping[str, str]:
request = context.request_context.request
headers = getattr(request, "headers", None)
if headers is None:
raise PermissionError("connector transport context is unavailable")
return {str(key).lower(): str(value) for key, value in headers.items()}
def run_asgi(application: Any) -> None:
import uvicorn
uvicorn.run(
application,
host="0.0.0.0", # noqa: S104 - container network binding is intentional.
port=int(os.environ.get("PORT", "8080")),
access_log=False,
proxy_headers=False,
server_header=False,
)
def load_json_environment(name: str, expected: type[list[Any]] | type[dict[str, Any]]) -> Any:
raw = os.environ.get(name, "")
try:
value = json.loads(raw)
except (TypeError, ValueError) as exc:
raise ValueError(f"{name} is invalid") from exc
if not isinstance(value, expected):
raise ValueError(f"{name} has an invalid shape")
return value
def read_regular_json(path: Path) -> dict[str, Any]:
if not path.is_file() or path.is_symlink():
raise ValueError("registered configuration file is unavailable")
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise ValueError("registered configuration file is invalid")
return value
def hub_database_url() -> str:
password = read_secret_file(
Path(os.environ.get("HUB_DATABASE_PASSWORD_FILE", "/run/secrets/hub_database_password"))
)
host = os.environ.get("HUB_DATABASE_HOST", "postgres")
database = os.environ.get("HUB_DATABASE_NAME", "itworx_mcp_hub")
user = os.environ.get("HUB_DATABASE_USER", "itworx_mcp_hub")
return f"postgresql+psycopg://{quote_plus(user)}:{quote_plus(password)}@{host}:5432/{database}"