auth.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. from __future__ import annotations
  2. import time
  3. from datetime import datetime, timezone
  4. from typing import Any
  5. from .db import read_sys_config_value, write_sys_config_value
  6. from .http_client import request_json
  7. PROJECTS_CONFIG_KEY = "mcp_data_collector_projects"
  8. PROJECT_TOKEN_CONFIG_KEY_PREFIX = "mcp_data_collector_token_"
  9. PROJECT_AUTH_LOGIN_PATH = "/api/ai/auth/password_login"
  10. def _to_iso_utc(timestamp: float) -> str:
  11. return datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat().replace("+00:00", "Z")
  12. def _normalize_project_key(raw_value: Any) -> str:
  13. project_key = str(raw_value or "").strip()
  14. if not project_key:
  15. raise ValueError("project_key is required")
  16. return project_key
  17. def _normalize_base_url(raw_value: Any, field_name: str) -> str:
  18. base_url = str(raw_value or "").strip().rstrip("/")
  19. if not base_url:
  20. raise ValueError(f"{field_name} is required")
  21. return base_url
  22. def _coerce_bool(raw_value: Any, default: bool) -> bool:
  23. if raw_value is None:
  24. return default
  25. if isinstance(raw_value, bool):
  26. return raw_value
  27. text = str(raw_value).strip().lower()
  28. if text in {"1", "true", "yes", "y", "on"}:
  29. return True
  30. if text in {"0", "false", "no", "n", "off"}:
  31. return False
  32. return default
  33. def _safe_int(raw_value: Any, field_name: str) -> int:
  34. try:
  35. return int(str(raw_value).strip())
  36. except Exception as exc:
  37. raise ValueError(f"invalid {field_name}: {raw_value}") from exc
  38. def _project_token_config_key(project_key: str) -> str:
  39. return f"{PROJECT_TOKEN_CONFIG_KEY_PREFIX}{project_key}"
  40. def load_projects_config() -> list[dict[str, Any]]:
  41. raw_value = read_sys_config_value(PROJECTS_CONFIG_KEY)
  42. if raw_value is None:
  43. raise ValueError(f"missing sys_config key: {PROJECTS_CONFIG_KEY}")
  44. if not isinstance(raw_value, list):
  45. raise ValueError(f"sys_config {PROJECTS_CONFIG_KEY} must be a JSON array")
  46. latest_by_key: dict[str, dict[str, Any]] = {}
  47. for item in raw_value:
  48. if not isinstance(item, dict):
  49. continue
  50. project_key = _normalize_project_key(item.get("project_key"))
  51. project_name = str(item.get("project_name") or project_key).strip() or project_key
  52. base_url = _normalize_base_url(item.get("base_url"), "base_url")
  53. data_collector_base_url = _normalize_base_url(
  54. item.get("data_collector_base_url"),
  55. "data_collector_base_url",
  56. )
  57. username = str(item.get("username") or "").strip()
  58. password = str(item.get("password") or "").strip()
  59. enabled = _coerce_bool(item.get("enabled"), True)
  60. latest_by_key[project_key] = {
  61. "project_key": project_key,
  62. "project_name": project_name,
  63. "base_url": base_url,
  64. "data_collector_base_url": data_collector_base_url,
  65. "username": username,
  66. "password": password,
  67. "enabled": enabled,
  68. }
  69. projects = list(latest_by_key.values())
  70. if not projects:
  71. raise ValueError(f"{PROJECTS_CONFIG_KEY} has no valid project entries")
  72. return projects
  73. def find_project_config(project_key: str) -> dict[str, Any]:
  74. expected = _normalize_project_key(project_key)
  75. for item in load_projects_config():
  76. if item["project_key"] != expected:
  77. continue
  78. if not item["enabled"]:
  79. raise ValueError(f"project '{expected}' is disabled")
  80. if not item["username"]:
  81. raise ValueError(f"project '{expected}' missing username")
  82. if not item["password"]:
  83. raise ValueError(f"project '{expected}' missing password")
  84. return item
  85. raise ValueError(f"project_key not found: {expected}")
  86. def _read_project_token_cache(project_key: str) -> dict[str, Any] | None:
  87. raw_value = read_sys_config_value(_project_token_config_key(project_key))
  88. if not isinstance(raw_value, dict):
  89. return None
  90. return raw_value
  91. def _write_project_token_cache(project_key: str, *, auth_token: str, expire_at: int) -> None:
  92. write_sys_config_value(
  93. _project_token_config_key(project_key),
  94. {
  95. "auth_token": auth_token,
  96. "expire_at": expire_at,
  97. "updated_at": _to_iso_utc(time.time()),
  98. },
  99. )
  100. def _login_project(project_cfg: dict[str, Any]) -> tuple[str, int]:
  101. payload = request_json(
  102. "POST",
  103. f"{project_cfg['base_url']}{PROJECT_AUTH_LOGIN_PATH}",
  104. json_payload={
  105. "username": project_cfg["username"],
  106. "password": project_cfg["password"],
  107. },
  108. )
  109. if not isinstance(payload, dict):
  110. raise ValueError("login failed: invalid response payload")
  111. errcode = payload.get("errcode")
  112. if str(errcode) not in {"0", "0.0"}:
  113. message = str(payload.get("msg") or payload.get("message") or "").strip()
  114. raise ValueError(f"login failed: {message or payload}")
  115. auth_token = str(payload.get("token") or "").strip()
  116. if not auth_token:
  117. raise ValueError("login failed: missing token")
  118. expire_raw = payload.get("token_expire_time")
  119. if expire_raw is None:
  120. expire_at = int(time.time()) + 3600
  121. else:
  122. expire_at = _safe_int(expire_raw, "token_expire_time")
  123. if expire_at <= int(time.time()):
  124. expire_at = int(time.time()) + 3600
  125. return auth_token, expire_at
  126. def resolve_project_token(project_cfg: dict[str, Any]) -> str:
  127. project_key = project_cfg["project_key"]
  128. cached = _read_project_token_cache(project_key)
  129. if isinstance(cached, dict):
  130. token = str(cached.get("auth_token") or "").strip()
  131. expire_raw = cached.get("expire_at")
  132. if token and expire_raw is not None:
  133. try:
  134. if int(time.time()) < int(str(expire_raw).strip()):
  135. return token
  136. except (TypeError, ValueError):
  137. pass
  138. auth_token, expire_at = _login_project(project_cfg)
  139. _write_project_token_cache(project_key, auth_token=auth_token, expire_at=expire_at)
  140. return auth_token