97 lines
5.0 KiB
Python
97 lines
5.0 KiB
Python
"""Reject unsafe release coordinates and keep secrets out of public release files."""
|
|
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
|
|
from dotenv import dotenv_values
|
|
|
|
from deploy.gray_config import GrayConfig, write_release
|
|
|
|
|
|
class GrayConfigTests(unittest.TestCase):
|
|
def settings(self):
|
|
return {
|
|
'PUBLIC_ORIGIN': 'https://chat.example.com', 'HOST_PORT': '29013',
|
|
'GRAY_DEPLOY_DIR': '/data/services/gray-logistics-chat',
|
|
'GRAY_PROXY_CONF_DIR': '/etc/nginx/conf.d',
|
|
'GRAY_TLS_CERT_FILE': '/etc/ssl/example/fullchain.pem',
|
|
'GRAY_TLS_KEY_FILE': '/etc/ssl/example/privkey.pem',
|
|
'SOURCE_COMMIT': 'a' * 40, 'BUILD_NUMBER': '7',
|
|
'CHAT_IMAGE': 'baiyue-logistics-chat-gray:' + 'a' * 12 + '-b7',
|
|
'CHAT_IMAGE_ID': 'sha256:' + 'b' * 64,
|
|
'CERTBOT_IMAGE_ID': 'sha256:' + 'c' * 64,
|
|
}
|
|
|
|
def test_unsafe_origins_paths_and_mismatched_images_are_rejected(self):
|
|
cases = (
|
|
('PUBLIC_ORIGIN', 'http://chat.example.com'),
|
|
('PUBLIC_ORIGIN', 'https://user:password@chat.example.com'),
|
|
('PUBLIC_ORIGIN', 'https://chat.example.com/admin'),
|
|
('GRAY_DEPLOY_DIR', '/data/services/../gray-logistics-chat'),
|
|
('GRAY_DEPLOY_DIR', '/data/shared-services'),
|
|
('GRAY_PROXY_CONF_DIR', '/etc/nginx; echo unsafe'),
|
|
('HOST_PORT', '443'), ('CHAT_IMAGE', 'another-service:latest'),
|
|
('SOURCE_COMMIT', 'main'), ('CHAT_IMAGE_ID', 'unverified-image'),
|
|
)
|
|
for key, value in cases:
|
|
with self.subTest(setting=key, value=value):
|
|
settings = self.settings()
|
|
settings[key] = value
|
|
with self.assertRaises(ValueError):
|
|
GrayConfig.from_environment(settings)
|
|
|
|
def test_data_paths_remain_stable_across_releases_and_credentials_stay_separate(self):
|
|
config = GrayConfig.from_environment(self.settings())
|
|
updated = replace(config, release=config.root + '/releases/next-build')
|
|
for release in (config, updated):
|
|
self.assertIn('CHAT_DATA_DIR=' + config.root + '/data\n', release.release_environment())
|
|
self.assertIn('CHAT_LOG_DIR=' + config.root + '/logs\n', release.release_environment())
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / 'release'
|
|
key = "fixture-${literal}-don't-expand"
|
|
write_release(path, config, key)
|
|
self.assertEqual(dotenv_values(path / 'runtime.env', interpolate=False)['DEEPSEEK_API_KEY'], key)
|
|
for name in ('release.env', 'site.conf', 'compose.yml', 'activate.sh'):
|
|
self.assertNotIn(key, (path / name).read_text(encoding='utf-8'))
|
|
self.assertIn('auth_basic_user_file', (path / 'site.conf').read_text(encoding='utf-8'))
|
|
|
|
def test_invalid_model_credential_does_not_create_a_release_directory(self):
|
|
config = GrayConfig.from_environment(self.settings())
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / 'release'
|
|
with self.assertRaises(ValueError):
|
|
write_release(path, config, 'fixture-key\nANOTHER_SETTING=bad')
|
|
self.assertFalse(path.exists())
|
|
|
|
def test_baidu_credentials_only_enter_private_runtime_file(self):
|
|
config = GrayConfig.from_environment(self.settings())
|
|
speech = {'ASR_PROVIDER': 'baidu', 'BAIDU_ASR_API_KEY': 'fixture-${asr-key}',
|
|
'BAIDU_ASR_SECRET_KEY': "fixture-asr-secret's", 'FFMPEG_BIN': r'C:\local\ffmpeg.exe'}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / 'release'
|
|
write_release(path, config, 'fixture-model-key', speech)
|
|
runtime = dotenv_values(path / 'runtime.env', interpolate=False)
|
|
self.assertEqual(runtime['ASR_PROVIDER'], 'baidu')
|
|
for key in ('BAIDU_ASR_API_KEY', 'BAIDU_ASR_SECRET_KEY'):
|
|
self.assertEqual(runtime[key], speech[key])
|
|
for file in path.iterdir():
|
|
if file.name != 'runtime.env':
|
|
self.assertNotIn(speech[key], file.read_text(encoding='utf-8'))
|
|
self.assertNotIn('FFMPEG_BIN', runtime)
|
|
|
|
def test_incomplete_or_multiline_speech_credentials_are_rejected_before_writing(self):
|
|
config = GrayConfig.from_environment(self.settings())
|
|
for speech in ({'ASR_PROVIDER': 'baidu'}, {'ASR_PROVIDER': 'unknown'},
|
|
{'ASR_PROVIDER': 'baidu', 'BAIDU_ASR_API_KEY': 'key', 'BAIDU_ASR_SECRET_KEY': 'secret\nOTHER=bad'}):
|
|
with self.subTest(provider=speech['ASR_PROVIDER']), tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / 'release'
|
|
with self.assertRaises(ValueError):
|
|
write_release(path, config, 'fixture-key', speech)
|
|
self.assertFalse(path.exists())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|