fix: do not echo setting values in configuration errors; test: stdio and config redaction

This commit is contained in:
Latte committed 2026-10-14 17:52:07 +02:00
1 parent 984673e8cf
commit 0f5f5cdd95
2 files changed
+87 -6

No files matched your search

+5 -6
View File
@@ -336,10 +336,11 @@ class Settings(BaseSettings):
"""Accept only granular Gitea scopes (unknown scopes make Gitea grant full access)."""
unknown = sorted({scope for scope in value if scope not in GITEA_SCOPES})
if unknown:
# The offending values are not echoed: a misplaced secret must not reach stderr.
raise ValueError(
f"OAUTH_SCOPES contains unknown scope(s): {', '.join(unknown)}. "
"Gitea grants full access for unknown scopes, so only granular "
"read:/write: scopes are accepted"
f"OAUTH_SCOPES contains {len(unknown)} unknown scope(s). Gitea grants full "
"access for unknown scopes, so only granular read:/write: scopes are accepted "
f"({', '.join(sorted(GITEA_SCOPES))})"
)
if not value:
raise ValueError("OAUTH_SCOPES must not be empty")
@@ -365,9 +366,7 @@ class Settings(BaseSettings):
unknown = sorted({item for item in lowered if item != "all" and item not in TOOL_DOMAINS})
if unknown:
allowed = ", ".join(sorted(TOOL_DOMAINS | {"all"}))
raise ValueError(
f"TOOLSETS contains unknown domain(s): {', '.join(unknown)}; use {allowed}"
)
raise ValueError(f"TOOLSETS contains {len(unknown)} unknown domain(s); use {allowed}")
if not lowered:
raise ValueError("TOOLSETS must not be empty (use 'all')")
return list(dict.fromkeys(lowered))
+82
View File
@@ -304,3 +304,85 @@ def test_secrets_in_object_keys_are_masked_like_values() -> None:
assert token not in json.dumps(masked) and token not in json.dumps(blocked)
assert masked["outer"][0]["plain"] != token
assert blocked != nested
# Configuration errors and a real stdio process
@pytest.mark.parametrize(
("variable", "value"),
[
("GITEA_URL", "https://user:" + GITEA_TOKEN + "@gitea.example.com"),
("GITEA_URL", "https://gitea.example.com?token=" + GITEA_TOKEN),
("MCP_BEARER_TOKEN", "short-" + "x" * 5),
("PUBLIC_BASE_URL", "http://mcp.example.com/" + GITEA_TOKEN),
("OAUTH_SCOPES", GITEA_TOKEN),
("TOKEN_ENCRYPTION_KEY", "tooshort-" + ENCRYPTION_KEY[:5]),
("MCP_PORT", GITEA_TOKEN),
("MCP_PATH", GITEA_TOKEN),
("FORWARDED_ALLOW_IPS", GITEA_TOKEN),
("LOG_LEVEL", GITEA_TOKEN),
],
)
def test_configuration_errors_name_the_setting_but_never_echo_its_value(
variable: str,
value: str,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
from aegis_gitea_mcp import cli
from aegis_gitea_mcp.cli import main
monkeypatch.setenv("GITEA_URL", "https://gitea.example.com")
monkeypatch.setenv("GITEA_TOKEN", GITEA_TOKEN)
monkeypatch.setenv(variable, value)
monkeypatch.setattr(cli, "configure_logging", lambda level: None)
assert main(["serve", "--transport", "http"]) == 2
err = capsys.readouterr().err
assert variable in err
assert value not in err and GITEA_TOKEN not in err
assert "Traceback" not in err
async def test_a_real_stdio_process_keeps_every_secret_out_of_stdout_stderr_and_the_audit_file(
tmp_path: Any,
) -> None:
import sys
from mcp import Client
from mcp.client.stdio import StdioServerParameters, stdio_client
secrets = [GITEA_TOKEN, BEARER_TOKEN, APP_CLIENT_SECRET, ENCRYPTION_KEY]
env = {
"GITEA_URL": "http://127.0.0.1:9", # nothing listens: every call fails
"GITEA_TOKEN": GITEA_TOKEN,
"MCP_BEARER_TOKEN": BEARER_TOKEN,
"GITEA_OAUTH_CLIENT_SECRET": APP_CLIENT_SECRET,
"TOKEN_ENCRYPTION_KEY": ENCRYPTION_KEY,
"DATA_DIR": str(tmp_path / "state"),
"AUDIT_LOG_PATH": str(tmp_path / "state" / "audit.log"),
"REQUEST_TIMEOUT_SECONDS": "2",
"LOG_LEVEL": "DEBUG",
}
params = StdioServerParameters(
command=sys.executable,
args=["-m", "aegis_gitea_mcp.cli", "serve", "--transport", "stdio"],
env=env,
cwd=str(tmp_path),
)
stderr_path = tmp_path / "stderr.log"
with stderr_path.open("w", encoding="utf-8") as errlog:
async with Client(stdio_client(params, errlog=errlog)) as client:
results = [
await client.call_tool("gitea_repo_get", {"owner": "o", "repo": "r"}),
await client.call_tool("gitea_repo_get", {"owner": 5}),
await client.call_tool("gitea_api_request", {"method": "GET", "path": "/user"}),
]
for result in results:
assert result.is_error
assert_clean(result.content[0].text, secrets) # type: ignore[union-attr]
assert_clean(stderr_path.read_text(encoding="utf-8"), secrets)
assert_clean((tmp_path / "state" / "audit.log").read_text(encoding="utf-8"), secrets)