fix: merge split gateway pairing stores
parent
2c5762f575
commit
8a7d0790df
|
|
@ -33,7 +33,7 @@ from gateway.whatsapp_identity import (
|
||||||
expand_whatsapp_aliases,
|
expand_whatsapp_aliases,
|
||||||
normalize_whatsapp_identifier,
|
normalize_whatsapp_identifier,
|
||||||
)
|
)
|
||||||
from hermes_constants import get_hermes_dir
|
from hermes_constants import get_hermes_dir, get_hermes_home
|
||||||
from utils import atomic_replace
|
from utils import atomic_replace
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -161,6 +161,51 @@ def _sync_allowlist_remove(platform: str, user_id: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json_file(path: Path) -> dict:
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_pairing_dir(active_dir: Path, alternate_dir: Path) -> None:
|
||||||
|
"""Merge split legacy/new pairing data into the active PairingStore dir.
|
||||||
|
|
||||||
|
Older installs use ``{HERMES_HOME}/pairing`` while newer code/docs may
|
||||||
|
write ``{HERMES_HOME}/platforms/pairing``. If both directories exist, the
|
||||||
|
gateway must not silently ignore approved users sitting in the inactive
|
||||||
|
location; otherwise already-paired Feishu users get asked for a fresh code.
|
||||||
|
"""
|
||||||
|
if not alternate_dir.exists() or active_dir.resolve() == alternate_dir.resolve():
|
||||||
|
return
|
||||||
|
active_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for src in alternate_dir.glob("*.json"):
|
||||||
|
if not src.is_file():
|
||||||
|
continue
|
||||||
|
dest = active_dir / src.name
|
||||||
|
merged = _load_json_file(src)
|
||||||
|
if not merged:
|
||||||
|
continue
|
||||||
|
current = _load_json_file(dest)
|
||||||
|
before = dict(current)
|
||||||
|
# Active data wins on key conflict; otherwise union the inactive data.
|
||||||
|
merged.update(current)
|
||||||
|
if merged != before:
|
||||||
|
_secure_write(dest, json.dumps(merged, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_split_pairing_dirs() -> None:
|
||||||
|
home = get_hermes_home()
|
||||||
|
old_dir = home / "pairing"
|
||||||
|
new_dir = home / "platforms" / "pairing"
|
||||||
|
active = PAIRING_DIR
|
||||||
|
alternate = new_dir if active.resolve() == old_dir.resolve() else old_dir
|
||||||
|
_merge_pairing_dir(active, alternate)
|
||||||
|
|
||||||
|
|
||||||
def _secure_write(path: Path, data: str) -> None:
|
def _secure_write(path: Path, data: str) -> None:
|
||||||
"""Write data to file with restrictive permissions (owner read/write only).
|
"""Write data to file with restrictive permissions (owner read/write only).
|
||||||
|
|
||||||
|
|
@ -212,6 +257,11 @@ class PairingStore:
|
||||||
else:
|
else:
|
||||||
self._dir = PAIRING_DIR
|
self._dir = PAIRING_DIR
|
||||||
self._dir.mkdir(parents=True, exist_ok=True)
|
self._dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not profile:
|
||||||
|
# Heal installs whose global pairing data ended up split across
|
||||||
|
# the legacy and new directories (per-profile stores never had
|
||||||
|
# the legacy/new split).
|
||||||
|
_migrate_split_pairing_dirs()
|
||||||
# Protects all read-modify-write cycles. The gateway runs multiple
|
# Protects all read-modify-write cycles. The gateway runs multiple
|
||||||
# platform adapters concurrently in threads sharing one PairingStore.
|
# platform adapters concurrently in threads sharing one PairingStore.
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,48 @@ def _make_store(tmp_path):
|
||||||
return PairingStore()
|
return PairingStore()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSplitPairingDirMigration:
|
||||||
|
def test_merges_new_approved_into_active_legacy_dir(self, tmp_path):
|
||||||
|
home = tmp_path / "home"
|
||||||
|
legacy = home / "pairing"
|
||||||
|
new = home / "platforms" / "pairing"
|
||||||
|
legacy.mkdir(parents=True)
|
||||||
|
new.mkdir(parents=True)
|
||||||
|
(new / "feishu-approved.json").write_text(json.dumps({
|
||||||
|
"ou_user": {"user_name": "Alice", "approved_at": 123.0}
|
||||||
|
}))
|
||||||
|
|
||||||
|
with patch("gateway.pairing.PAIRING_DIR", legacy), patch("gateway.pairing.get_hermes_home", return_value=home):
|
||||||
|
store = PairingStore()
|
||||||
|
assert store.is_approved("feishu", "ou_user") is True
|
||||||
|
|
||||||
|
migrated = json.loads((legacy / "feishu-approved.json").read_text())
|
||||||
|
assert "ou_user" in migrated
|
||||||
|
|
||||||
|
def test_active_entries_win_when_merging_split_dirs(self, tmp_path):
|
||||||
|
home = tmp_path / "home"
|
||||||
|
legacy = home / "pairing"
|
||||||
|
new = home / "platforms" / "pairing"
|
||||||
|
legacy.mkdir(parents=True)
|
||||||
|
new.mkdir(parents=True)
|
||||||
|
(legacy / "feishu-approved.json").write_text(json.dumps({
|
||||||
|
"ou_user": {"user_name": "Active", "approved_at": 2.0}
|
||||||
|
}))
|
||||||
|
(new / "feishu-approved.json").write_text(json.dumps({
|
||||||
|
"ou_user": {"user_name": "Inactive", "approved_at": 1.0},
|
||||||
|
"ou_other": {"user_name": "Other", "approved_at": 1.0},
|
||||||
|
}))
|
||||||
|
|
||||||
|
with patch("gateway.pairing.PAIRING_DIR", legacy), patch("gateway.pairing.get_hermes_home", return_value=home):
|
||||||
|
store = PairingStore()
|
||||||
|
assert store.is_approved("feishu", "ou_user") is True
|
||||||
|
assert store.is_approved("feishu", "ou_other") is True
|
||||||
|
|
||||||
|
migrated = json.loads((legacy / "feishu-approved.json").read_text())
|
||||||
|
assert migrated["ou_user"]["user_name"] == "Active"
|
||||||
|
assert migrated["ou_other"]["user_name"] == "Other"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _secure_write
|
# _secure_write
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue