video_monitor/tests/test_phase1_security.py
2026-08-30 22:23:12 +08:00

177 lines
7.3 KiB
Python

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()