admin / Syanpse-Vanguard
public
Syanpse-Vanguard / synapse-vanguard-v3 / sv / ingress.py
3334 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | """Ingress API key management. An external Synapse-suite app (e.g. Synapse IronNode) authenticates to POST /v1/ingest/events with a Bearer key. The key is generated here, stored vault-encrypted (AES-256-GCM) in the settings table so it survives restarts, and only overwritten when an admin explicitly regenerates it. The plaintext is retrievable by admins for copy-out (so it can be pasted into the sending app) but is never logged.""" from __future__ import annotations import hmac import json import secrets from datetime import datetime, timezone from sv.config import DEFAULT_TENANT_ID from sv.storage.base import Query, StorageEngine from sv.vault import EncryptedBlob, decrypt_json, encrypt_json _KEY = "ingress_api" _PREFIX = "svg_" # human-recognizable key prefix def _now() -> datetime: return datetime.now(timezone.utc).replace(tzinfo=None) def new_key() -> str: return _PREFIX + secrets.token_urlsafe(32) async def get_config(engine: StorageEngine, tenant_id=DEFAULT_TENANT_ID) -> dict | None: """Return {api_key, enabled, updated_ts} decrypted, or None if never set.""" row = await engine.fetchone(Query( "SELECT value, updated_ts FROM settings WHERE tenant_id = ? AND key = ?", [str(tenant_id), _KEY], )) if not row or not row["value"]: return None stored = json.loads(row["value"]) if isinstance(row["value"], str) else row["value"] try: secret = decrypt_json(EncryptedBlob(**stored["blob"])) except Exception: return None return { "api_key": secret["api_key"], "enabled": bool(stored.get("enabled", True)), "updated_ts": row["updated_ts"], } async def save(engine: StorageEngine, api_key: str, enabled: bool, tenant_id=DEFAULT_TENANT_ID) -> None: blob = encrypt_json({"api_key": api_key}) value = json.dumps({ "enabled": enabled, # keys match the EncryptedBlob dataclass fields for a clean round-trip "blob": {"iv": blob.iv, "auth_tag": blob.auth_tag, "ciphertext": blob.ciphertext}, }) # UPSERT — DuckDB supports INSERT ... ON CONFLICT DO UPDATE. await engine.run(Query( "INSERT INTO settings (tenant_id, key, value, updated_ts) VALUES (?, ?, ?, ?) " "ON CONFLICT (tenant_id, key) DO UPDATE SET value = EXCLUDED.value, " "updated_ts = EXCLUDED.updated_ts", [str(tenant_id), _KEY, value, _now()], )) async def generate(engine: StorageEngine, tenant_id=DEFAULT_TENANT_ID) -> str: """Create a fresh key (overwriting any existing) and enable ingress.""" key = new_key() await save(engine, key, enabled=True, tenant_id=tenant_id) return key async def set_enabled(engine: StorageEngine, enabled: bool, tenant_id=DEFAULT_TENANT_ID) -> bool: cfg = await get_config(engine, tenant_id) if not cfg: return False await save(engine, cfg["api_key"], enabled, tenant_id) return True async def verify(engine: StorageEngine, presented: str, tenant_id=DEFAULT_TENANT_ID) -> bool: """Constant-time check of a presented key against the stored, enabled key.""" cfg = await get_config(engine, tenant_id) if not cfg or not cfg["enabled"]: return False return hmac.compare_digest(presented, cfg["api_key"]) |