eon: use saved session

This commit is contained in:
Nirvana
2026-10-22 07:47:13 +02:00
parent be556cb89b
commit 9d23cd8ac7
2 changed files with 61 additions and 24 deletions
@@ -51,7 +51,6 @@ typed errors only, nothing secret in logs (lengths at most).
"""
import hashlib
import json
import secrets
import string
import threading
@@ -4,7 +4,10 @@ Glue between EonAuth and the framework's storage managers.
SessionStore -- EonStore on top of SessionManager: one dict at
session.json[<provider>][<country>] (country LOWER-case, as
the managers key by it). Read once, written through.
the managers key by it). STATELESS view: every get / set /
delete re-reads through SessionManager, so keys this process
did not write are preserved and the store never overwrites
the on-disk entry with a stale snapshot.
default_persistence() -- finds a SessionManager and a credentials loader the
way BaseAuthenticator does (injected settings manager, else
a SettingsManager of its own); anything missing degrades to
@@ -15,6 +18,20 @@ SessionManager logs every value of keys it does not know at INFO
(`Saving session data ...`): never hand this store a secret that can be
re-derived. EonAuth persists device identity and the refresh token only (the
refresh token is on SessionManager's mask list).
Why there is NO local cache here
--------------------------------
SessionManager.load_session re-reads session.json on every call and
save_session REPLACES the dict at session.json[provider][country]. A local
cache that is populated on the first read and written through on set would
therefore:
* hide keys written to disk after the cache was populated (e.g. another
provider, another process, or the same provider in a previous run), and
* on the first set(), save the stale cache back and ERASE those keys.
That is exactly how an existing eon device identity got wiped and a new device
was registered (auth_device_forbidden). Read-modify-write on every mutation.
"""
import threading
@@ -25,51 +42,66 @@ from .auth import EonStore, MemoryStore
class SessionStore:
"""EonStore backed by SessionManager, with no local state.
Each operation goes to SessionManager, which reads session.json and (for
writes) replaces the provider/country sub-dict. The re-read inside
set_many / delete makes the write a merge into the CURRENT on-disk state
instead of a write-through of a snapshot taken at process start.
"""
def __init__(self, session_manager: Any, provider: str, country: Optional[str]):
self._sm = session_manager
self._provider = provider
self._country = country or None
# Normalise here: a caller passing "BA" must not open a parallel
# eon.BA subtree next to the eon.ba one the managers use.
self._country = country.lower() if country else None
self._lock = threading.RLock()
self._cache: Optional[Dict[str, str]] = None
def _data(self) -> Dict[str, str]:
if self._cache is None:
loaded = self._sm.load_session(self._provider, self._country)
self._cache = {k: v for k, v in (loaded or {}).items() if isinstance(v, str)}
return self._cache
# ------------------------------------------------------------------
def _read(self) -> Dict[str, str]:
"""Current on-disk dict for provider/country (never cached)."""
loaded = self._sm.load_session(self._provider, self._country)
if not loaded:
return {}
return {k: v for k, v in loaded.items() if isinstance(v, str)}
def get(self, key: str) -> Optional[str]:
with self._lock:
return self._data().get(key)
return self._read().get(key)
def set(self, key: str, value: str) -> None:
self.set_many({key: value})
def set_many(self, values: Dict[str, str]) -> None:
with self._lock:
data = self._data()
data = self._read()
if all(data.get(k) == v for k, v in values.items()):
return
data.update(values)
self._save()
if not self._sm.save_session(self._provider, data, self._country):
logger.warning(f"{self._provider}: could not persist the session")
def delete(self, key: str) -> None:
with self._lock:
data = self._data()
if key in data:
del data[key]
self._save()
def _save(self) -> None:
# save_session REPLACES the dict at that path: write the whole cache.
if not self._sm.save_session(self._provider, dict(self._data()), self._country):
logger.warning(f"{self._provider}: could not persist the session")
data = self._read()
if key not in data:
return
del data[key]
if not self._sm.save_session(self._provider, data, self._country):
logger.warning(f"{self._provider}: could not persist the session")
def default_persistence(
*, provider: str, country: str, settings_manager: Any = None,
) -> Tuple[EonStore, Optional[Callable[[], Any]]]:
"""(store, credentials_loader); never raises."""
"""(store, credentials_loader); never raises.
The SessionManager is the SHARED one when the settings manager exposes it;
a fresh SessionManager() is only a last resort (it still reads the same
session.json via VFS) and is logged, because a silently different store is
how a provider ends up registering a brand-new device identity.
"""
manager = settings_manager
if manager is None:
try:
@@ -83,13 +115,19 @@ def default_persistence(
except Exception as exc:
logger.warning(f"{provider}: could not register with the settings manager: {exc!r}")
session_manager = getattr(manager, "session_manager", None)
session_manager = getattr(manager, "session_manager", None) if manager is not None else None
if session_manager is None:
logger.warning(
f"{provider}: settings manager exposes no session_manager; "
f"creating a standalone SessionManager (same session.json, different instance)"
)
try:
from ...base.auth.session_manager import SessionManager
session_manager = SessionManager()
except Exception as exc:
logger.warning(f"{provider}: no SessionManager ({exc!r}); device identity is NOT persisted")
logger.warning(
f"{provider}: no SessionManager ({exc!r}); device identity is NOT persisted"
)
store: EonStore = (
SessionStore(session_manager, provider, country) if session_manager is not None