Corp/ops/security/tests/test_key_rotation.py
Игорь Надеин 4f6244ab1f
Some checks failed
Deploy production / deploy (push) Failing after 2s
Fix CORP runtime errors and harden deployment and media access
2026-09-10 16:40:08 +03:00

96 lines
4.2 KiB
Python

"""Offline key-rotation helper tests; only generated dummy keys and temp files."""
import importlib.util
import io
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
@unittest.skipUnless(os.name == 'posix', 'Linux descriptor/xattr protections')
class KeyRotationTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
path = Path(__file__).resolve().parents[1]/'rotate-django-key.py'
spec = importlib.util.spec_from_file_location('rotate_django_key', path)
cls.rotation = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cls.rotation)
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='key-rotation-test-')
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.env = self.root/'.env'
self.env.write_text('dummy-old-content\n')
self.env.chmod(0o640)
def test_atomic_update_preserves_mode_and_user_xattr(self):
os.setxattr(self.env, 'user.rotation-test', b'preserve')
self.rotation.atomic_environment_update(self.env, 'dummy-old-content\n', 'dummy-new-content\n')
self.assertEqual(self.env.read_text(), 'dummy-new-content\n')
self.assertEqual(self.env.stat().st_mode & 0o777, 0o640)
self.assertEqual(os.getxattr(self.env, 'user.rotation-test'), b'preserve')
self.assertFalse(list(self.root.glob('.env.signing-key-*')))
def test_concurrent_content_change_aborts(self):
with self.assertRaisesRegex(RuntimeError, 'changed concurrently'):
self.rotation.atomic_environment_update(self.env, 'outdated-content', 'new')
self.assertEqual(self.env.read_text(), 'dummy-old-content\n')
def test_env_symlink_is_not_followed(self):
target = self.root/'victim'
target.write_text('unchanged')
self.env.unlink()
self.env.symlink_to(target)
with self.assertRaises(OSError):
self.rotation.atomic_environment_update(self.env, 'unchanged', 'new')
self.assertEqual(target.read_text(), 'unchanged')
def test_parent_symlink_is_not_followed(self):
parent = self.root/'alias'
parent.symlink_to(self.root, target_is_directory=True)
with self.assertRaises(OSError):
self.rotation.read_environment(parent/'.env')
def test_staging_symlink_swap_cannot_write_or_chown_victim(self):
target = self.root/'victim'
target.write_text('unchanged')
target.chmod(0o644)
original = os.fchown
def replace_staging(descriptor, uid, gid):
staged, = self.root.glob('.env.signing-key-*')
staged.unlink()
staged.symlink_to(target)
original(descriptor, uid, gid)
with patch.object(self.rotation.os, 'fchown', side_effect=replace_staging):
with self.assertRaisesRegex(RuntimeError, 'staging file was replaced'):
self.rotation.atomic_environment_update(self.env, 'dummy-old-content\n', 'new')
self.assertEqual(target.read_text(), 'unchanged')
self.assertEqual(target.stat().st_mode & 0o777, 0o644)
self.assertEqual(self.env.read_text(), 'dummy-old-content\n')
def test_fallbacks_must_be_list_of_nonempty_strings(self):
for value in ('{}', '"key"', '[1]', '[""]', 'null'):
with self.subTest(value=value), self.assertRaises(ValueError):
self.rotation.parse_fallbacks(value)
self.assertEqual(self.rotation.parse_fallbacks('["dummy-old"]'), ['dummy-old'])
def test_dotenv_json_escaping_round_trip(self):
from dotenv import dotenv_values
value = json.dumps(["dummy'quote\\backslash", 'dummy-old'])
source = self.rotation.render_values('DJANGO_SECRET_KEY_FALLBACKS=[]\n', {
'DJANGO_SECRET_KEY_FALLBACKS': value,
'JWT_SIGNING_KEY': "dummy'quote\\backslash",
})
parsed = dotenv_values(stream=io.StringIO(source))
self.assertEqual(json.loads(parsed['DJANGO_SECRET_KEY_FALLBACKS']), ["dummy'quote\\backslash", 'dummy-old'])
self.assertEqual(parsed['JWT_SIGNING_KEY'], "dummy'quote\\backslash")
if __name__ == '__main__':
unittest.main()