video_monitor/tests/test_phase1_security.py

177 lines
7.3 KiB
Python
Raw Normal View History

2026-08-30 22:22:11 +08:00
import importlib
import json
import os
import sys
import tempfile
import types
import unittest
from unittest import mock
import shutil
import sqlite3
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
class RuntimeSecretsTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
from app.utils import Secrets
self.module = Secrets
self.old_path = Secrets.RUNTIME_SECRETS_FILE
self.old_cache = Secrets._CACHE
Secrets.RUNTIME_SECRETS_FILE = Path(self.temp.name) / "runtime.json"
Secrets._CACHE = None
def tearDown(self):
self.module.RUNTIME_SECRETS_FILE = self.old_path
self.module._CACHE = self.old_cache
self.temp.cleanup()
def test_generates_unique_nonempty_secrets_without_public_config(self):
internal = self.module.get_runtime_secret("internal_api_secret")
media = self.module.get_runtime_secret("media_secret")
self.assertGreaterEqual(len(internal), 40)
self.assertGreaterEqual(len(media), 32)
self.assertNotEqual(internal, media)
data = json.loads(self.module.RUNTIME_SECRETS_FILE.read_text(encoding="utf-8"))
self.assertEqual(data["internal_api_secret"], internal)
def test_environment_value_overrides_file(self):
key = "MONITOR_INTERNAL_API_SECRET"
old = os.environ.get(key)
os.environ[key] = "environment-secret-value"
try:
self.assertEqual(
self.module.get_runtime_secret("internal_api_secret"),
"environment-secret-value",
)
finally:
if old is None:
os.environ.pop(key, None)
else:
os.environ[key] = old
class _FakeRequest:
def __init__(self, remote="127.0.0.1", method="POST", path="/inner/test", body=b"{}", headers=None, token=""):
self.META = {"REMOTE_ADDR": remote}
self.method = method
self.path_info = path
self.body = body
self.headers = headers or {}
self.GET = {"token": token} if token else {}
class InternalRequestAuthTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
try:
import django.conf # noqa: F401
except ImportError:
django = types.ModuleType("django")
conf = types.ModuleType("django.conf")
conf.settings = types.SimpleNamespace(MONITOR_INTERNAL_API_SECRET="test-internal-secret")
django.conf = conf
sys.modules["django"] = django
sys.modules["django.conf"] = conf
cls.security = importlib.import_module("app.security")
cls.security.settings.MONITOR_INTERNAL_API_SECRET = "test-internal-secret"
def test_valid_hmac_from_loopback_is_accepted(self):
now = 1_800_000_000
body = b'{"x":1}'
headers = self.security.build_internal_auth_headers("POST", "/inner/test", body, timestamp=now)
request = _FakeRequest(body=body, headers=headers)
self.assertTrue(self.security.verify_internal_request(request, now=now))
def test_non_loopback_is_rejected_even_with_valid_hmac(self):
now = 1_800_000_000
headers = self.security.build_internal_auth_headers("POST", "/inner/test", b"{}", timestamp=now)
request = _FakeRequest(remote="192.168.1.10", headers=headers)
self.assertFalse(self.security.verify_internal_request(request, now=now))
def test_tampered_body_and_stale_signature_are_rejected(self):
now = 1_800_000_000
headers = self.security.build_internal_auth_headers("POST", "/inner/test", b"{}", timestamp=now)
self.assertFalse(self.security.verify_internal_request(
_FakeRequest(body=b'{"tampered":1}', headers=headers), now=now
))
self.assertFalse(self.security.verify_internal_request(
_FakeRequest(headers=headers), now=now + 61
))
def test_zlm_token_requires_loopback(self):
self.assertTrue(self.security.verify_internal_request(
_FakeRequest(token="test-internal-secret")
))
self.assertFalse(self.security.verify_internal_request(
_FakeRequest(remote="10.0.0.2", token="test-internal-secret")
))
class SourceBoundaryTests(unittest.TestCase):
def test_public_config_contains_no_runtime_credentials(self):
config = json.loads((ROOT / "config.json").read_text(encoding="utf-8"))
self.assertNotIn("safe", config)
self.assertNotIn("mediaSecret", config)
self.assertNotIn("sipServerPass", config.get("sipServer", {}))
self.assertNotIn("sipServerNonce", config.get("sipServer", {}))
def test_snap_uses_no_shell_and_middleware_has_no_safe_bypass(self):
nvr = (ROOT / "app" / "views" / "NvrView.py").read_text(encoding="utf-8")
middleware = (ROOT / "app" / "middleware.py").read_text(encoding="utf-8")
self.assertIn("shell=False", nvr)
self.assertNotIn("shell=True", nvr)
self.assertNotIn("HTTP_SAFE", middleware)
self.assertNotIn("'/nvr/openSnap'", middleware)
def test_download_endpoint_no_longer_accepts_filename_parameter(self):
storage = (ROOT / "app" / "views" / "StorageView.py").read_text(encoding="utf-8")
self.assertIn('params.get("file_id"', storage)
self.assertNotIn('params.get("filename"', storage)
class SecureInitializeTests(unittest.TestCase):
def test_replaces_users_clears_sessions_and_preserves_business_data(self):
with tempfile.TemporaryDirectory() as temp_dir:
database = Path(temp_dir) / "test.sqlite3"
shutil.copy2(ROOT / "monitor.sqlite3", database)
before_con = sqlite3.connect(database)
try:
before_streams = before_con.execute("select count(*) from av_stream").fetchone()[0]
finally:
before_con.close()
spec = importlib.util.spec_from_file_location(
"secure_initialize_test", ROOT / "scripts" / "secure_initialize.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
argv = [
"secure_initialize.py", "--database", str(database),
"--admin-username", "secureadmin", "--admin-email", "admin@example.invalid",
]
with mock.patch.object(sys, "argv", argv), \
mock.patch.object(module.getpass, "getpass", side_effect=["Long-Test-Password-42!", "Long-Test-Password-42!"]), \
mock.patch("app.utils.Secrets.rotate_runtime_secrets", return_value=""):
module.main()
con = sqlite3.connect(database)
try:
users = con.execute("select username,is_superuser,is_staff,is_active from auth_user").fetchall()
sessions = con.execute("select count(*) from django_session").fetchone()[0]
after_streams = con.execute("select count(*) from av_stream").fetchone()[0]
finally:
con.close()
self.assertEqual(users, [("secureadmin", 1, 1, 1)])
self.assertEqual(sessions, 0)
self.assertEqual(after_streams, before_streams)
self.assertTrue(list(Path(temp_dir).glob("test.sqlite3.backup-*")))
if __name__ == "__main__":
unittest.main()