Catalyst / admin/Syanpse-Vanguard 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Syanpse-Vanguard

public
Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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"])