fix(channels): harden runtime credential management APIs (#3581)

* fix(channels): harden runtime credential management APIs

* fix(channels): address review feedback on credential hardening

Follow-up to the runtime credential-hardening pass, resolving five review
findings:

- WeChat auth persistence now writes through a 0o600 NamedTemporaryFile +
  Path.replace instead of write_text-then-chmod, so the iLink bot_token is
  never briefly readable at umask defaults (mirrors ChannelRuntimeConfigStore).
- The post-write chmod is split into its own try/except: a chmod failure on a
  filesystem without POSIX perms now logs at debug instead of masquerading as
  a "failed to persist" warning.
- Extracted the three near-identical _require_admin_user helpers (mcp,
  channel_connections, channels) into a single require_admin_user(request, *,
  detail) in app/gateway/deps.py; each router supplies its own detail string.
- Strengthened the runtime-config-store chmod coverage: a new test injects a
  temp-file chmod failure and asserts it is logged at debug while the
  destination is still owner-only (mutation-verified to fail if the chmod is
  dropped), plus a loose-pre-existing-file case.
- Removed the unused _FakeRepo from the blocking-io test: its isinstance gate
  routes through the repo-less 503 path, so neither stub was ever invoked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Nan Gao
2026-06-18 04:45:33 +02:00
committed by GitHub
parent 68ba4198b8
commit 2b301e8211
11 changed files with 314 additions and 56 deletions
@@ -51,6 +51,10 @@ class ChannelRuntimeConfigStore:
delete=False,
)
try:
try:
Path(fd.name).chmod(0o600)
except OSError:
logger.debug("Unable to chmod temporary channel runtime config store at %s", fd.name, exc_info=True)
json.dump(self._data, fd, indent=2, ensure_ascii=False)
fd.close()
Path(fd.name).replace(self._path)
+22 -1
View File
@@ -10,6 +10,7 @@ import json
import logging
import mimetypes
import secrets
import tempfile
import time
from collections.abc import Mapping
from enum import IntEnum
@@ -1376,9 +1377,29 @@ class WechatChannel(Channel):
if self._auth_path:
try:
self._auth_path.parent.mkdir(parents=True, exist_ok=True)
self._auth_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
# Write through a 0o600 temp file and atomically rename so the
# iLink bot_token is never briefly readable at umask defaults
# (mirrors ChannelRuntimeConfigStore._save). NamedTemporaryFile
# uses mkstemp, which creates the file at 0o600 from the start.
fd = tempfile.NamedTemporaryFile(mode="w", dir=self._auth_path.parent, suffix=".tmp", delete=False, encoding="utf-8")
try:
json.dump(data, fd, ensure_ascii=False, indent=2)
fd.close()
Path(fd.name).replace(self._auth_path)
except BaseException:
fd.close()
Path(fd.name).unlink(missing_ok=True)
raise
except OSError:
logger.warning("[WeChat] failed to persist auth state to %s", self._auth_path)
else:
# Hardening only; the destination already inherits 0o600 from the
# temp file. A chmod failure on filesystems without POSIX perms
# must not masquerade as a persist failure.
try:
self._auth_path.chmod(0o600)
except OSError:
logger.debug("[WeChat] unable to chmod auth state at %s", self._auth_path, exc_info=True)
return data
@staticmethod