NATS wildcards (node.*.capabilities.get) only work for subscriptions, not for publish. Switch to a dedicated broadcast subject (fabric.capabilities.discover) that all NCS instances subscribe to, enabling proper scatter-gather discovery across nodes. Made-with: Cursor
98 lines
3.6 KiB
Python
98 lines
3.6 KiB
Python
"""
|
||
common.py — shared auth helpers для farmOS tools.
|
||
|
||
farmOS 4.x підтримує тільки OAuth2 Bearer (не Basic Auth).
|
||
При наявності FARMOS_TOKEN — використовуємо його як статичний Bearer.
|
||
При наявності FARMOS_USER + FARMOS_PASS — отримуємо JWT через OAuth2
|
||
password grant і кешуємо його в пам'яті до закінчення TTL.
|
||
"""
|
||
import os
|
||
import time
|
||
import threading
|
||
import requests
|
||
|
||
# ── In-memory OAuth2 token cache (process-level, thread-safe) ────────────────
|
||
_token_cache: dict = {}
|
||
_token_lock = threading.Lock()
|
||
|
||
_OAUTH_TOKEN_PATH = "/oauth/token"
|
||
# Оновлюємо токен за 60 секунд до закінчення, щоб не отримати 401
|
||
_TOKEN_REFRESH_BUFFER_S = 60
|
||
|
||
|
||
def _farmos_base_url() -> str:
|
||
return os.getenv("FARMOS_BASE_URL", "").strip().rstrip("/")
|
||
|
||
|
||
def _fetch_oauth_token(base_url: str, user: str, pwd: str, client_id: str) -> str | None:
|
||
"""
|
||
Робить POST /oauth/token з password grant.
|
||
Fail-closed: будь-яка помилка → повертає None (без raise).
|
||
"""
|
||
try:
|
||
resp = requests.post(
|
||
base_url + _OAUTH_TOKEN_PATH,
|
||
data={
|
||
"grant_type": "password",
|
||
"username": user,
|
||
"password": pwd,
|
||
"client_id": client_id,
|
||
"scope": "",
|
||
},
|
||
timeout=5,
|
||
)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
access_token = data.get("access_token", "")
|
||
expires_in = float(data.get("expires_in", 3600))
|
||
if access_token:
|
||
with _token_lock:
|
||
_token_cache["access_token"] = access_token
|
||
_token_cache["expires_at"] = time.monotonic() + expires_in - _TOKEN_REFRESH_BUFFER_S
|
||
return access_token
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
def _get_cached_oauth_token(base_url: str, user: str, pwd: str, client_id: str) -> str | None:
|
||
"""
|
||
Повертає кешований токен або отримує новий якщо протух / відсутній.
|
||
"""
|
||
with _token_lock:
|
||
cached = _token_cache.get("access_token")
|
||
expires_at = _token_cache.get("expires_at", 0.0)
|
||
if cached and time.monotonic() < expires_at:
|
||
return cached
|
||
|
||
# Поза lock — робимо мережевий запит
|
||
return _fetch_oauth_token(base_url, user, pwd, client_id)
|
||
|
||
|
||
def _auth_headers() -> dict:
|
||
"""
|
||
Повертає заголовки авторизації для farmOS JSON:API запитів.
|
||
|
||
Пріоритет:
|
||
1. FARMOS_TOKEN (статичний Bearer — для dev/тестів або PAT)
|
||
2. FARMOS_USER + FARMOS_PASS → OAuth2 password grant → Bearer JWT
|
||
3. Порожній dict (fail-closed: запит піде без auth, farmOS поверне 403/401)
|
||
"""
|
||
# 1. Статичний токен
|
||
token = os.getenv("FARMOS_TOKEN", "").strip()
|
||
if token:
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
# 2. OAuth2 password grant
|
||
user = os.getenv("FARMOS_USER", "").strip()
|
||
pwd = os.getenv("FARMOS_PASS", os.getenv("FARMOS_PASSWORD", "")).strip()
|
||
client_id = os.getenv("FARMOS_CLIENT_ID", "farm").strip()
|
||
base_url = _farmos_base_url()
|
||
|
||
if user and pwd and base_url:
|
||
jwt = _get_cached_oauth_token(base_url, user, pwd, client_id)
|
||
if jwt:
|
||
return {"Authorization": f"Bearer {jwt}"}
|
||
|
||
return {}
|