Newer
Older
navi-1 / tests / unit / mcp / test_no_literal_secrets.py
"""The tracked MCP configs must never carry a live credential.

``mcp_servers.d/*.json`` is committed, so a token written there is in git
history for good — and ``GET /admin/mcp/config`` hands the same value to the
admin client. A config therefore *names* the variable (``Bearer ${NAVI_MCP_X_TOKEN}``)
and the value lives in the service ``.env``; see ``docs/mcp.md``.

This is the guard on that invariant: it is what fails when someone pastes a
token back into a config file, by hand or through ``PUT /admin/mcp/config``.
"""

import json
import re
from pathlib import Path

import navi.mcp.config as mcp_config
from navi.tools._internal.redact import is_sensitive_key

REPO_ROOT = Path(mcp_config.__file__).resolve().parents[2]
CONFIG_DIR = REPO_ROOT / "mcp_servers.d"

_PLACEHOLDER = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}")

# Non-credential settings a config may map out of `.env`. Not secrets: the
# placeholder is there so the value stays out of a tracked file (one `.env`
# serves every machine), and the fallback form keeps a host without one working
# — see `docs/mcp.md#secrets`. A setting belongs in `env`, never in a header.
SETTINGS_VARIABLES = {"SEARXNG_URL"}

# The credential-bearing servers, and the variable each one names. Keep in step
# with `deploy/env.template`.
EXPECTED_VARIABLES = {
    "gnexus-creds": "NAVI_MCP_GNEXUS_CREDS_TOKEN",
    "gntodo": "NAVI_MCP_GNTODO_TOKEN",
    "hard-panel": "NAVI_MCP_HARD_PANEL_TOKEN",
    "synapse": "NAVI_MCP_SYNAPSE_TOKEN",
    "tgclient": "NAVI_MCP_TGCLIENT_TOKEN",
}


def _names(value: object) -> list[str]:
    return [match.group(1) for match in _PLACEHOLDER.finditer(str(value))]



def _configs() -> dict[str, dict]:
    return {
        path.stem: json.loads(path.read_text(encoding="utf-8"))
        for path in sorted(CONFIG_DIR.glob("*.json"))
    }


def test_every_sensitive_header_or_env_value_is_a_placeholder():
    offenders = [
        f"{server}: {field}.{key}"
        for server, cfg in _configs().items()
        for field in ("headers", "env")
        for key, value in (cfg.get(field) or {}).items()
        if is_sensitive_key(key) and "${" not in str(value)
    ]
    assert not offenders, (
        "literal credential in a tracked config — replace it with "
        f"`${{NAVI_MCP_<SERVER>_TOKEN}}` and put the value in .env: {offenders}"
    )


def test_placeholder_names_are_credentials_or_known_settings():
    names = {
        name
        for cfg in _configs().values()
        for field in ("headers", "env")
        for value in (cfg.get(field) or {}).values()
        for name in _names(value)
    }
    assert names, "no placeholders in any config — did a token get pasted back in?"
    unexpected = [
        name
        for name in names
        if not name.startswith("NAVI_MCP_") and name not in SETTINGS_VARIABLES
    ]
    assert not unexpected, (
        "unexpected placeholder names — a credential is NAVI_MCP_*, a plain "
        f"setting must be listed in SETTINGS_VARIABLES: {sorted(unexpected)}"
    )


def test_settings_placeholders_live_in_env_blocks():
    """`headers` carry credentials; a SearXNG URL there would mean someone
    mapped a setting through the wrong field."""
    offenders = [
        f"{server}: headers.{key}"
        for server, cfg in _configs().items()
        for key, value in (cfg.get("headers") or {}).items()
        if any(name in SETTINGS_VARIABLES for name in _names(value))
    ]
    assert not offenders, f"a plain setting belongs in `env`: {offenders}"


def test_the_credential_servers_name_their_variable():
    configs = _configs()
    for server, variable in EXPECTED_VARIABLES.items():
        assert server in configs, f"{server}.json is gone — update this test with it"
        assert configs[server]["headers"]["Authorization"] == f"Bearer ${{{variable}}}"