Files
microdao-daarion/packages/agromatrix-tools/agromatrix_tools/common.py
Apple 90080c632a fix(fabric): use broadcast subject for NATS capabilities discovery
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
2026-02-27 03:20:13 -08:00

98 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 {}