diff --git a/app/contacts/contact_gist.py b/app/contacts/contact_gist.py index 6dfb2de12..cf87e1401 100644 --- a/app/contacts/contact_gist.py +++ b/app/contacts/contact_gist.py @@ -60,7 +60,7 @@ def retrieve_config(self): return self.token async def start(self): - token = self.get_config('app.contact.gist', '').strip() + token = (self.get_secret('app.contact.gist', env_var='CALDERA_GIST_TOKEN') or '').strip() if token: if self.valid_config(token): self.token = token diff --git a/app/contacts/contact_slack.py b/app/contacts/contact_slack.py index e194c03bc..899e86e57 100644 --- a/app/contacts/contact_slack.py +++ b/app/contacts/contact_slack.py @@ -61,9 +61,9 @@ def retrieve_config(self): async def start(self): if await self.valid_config(): - self.key = self.get_config('app.contact.slack.api_key') - self.channelid = self.get_config('app.contact.slack.channel_id') - self.botid = self.get_config('app.contact.slack.bot_id') + self.key = self.get_secret('app.contact.slack.api_key', env_var='CALDERA_SLACK_API_KEY') + self.channelid = self.get_secret('app.contact.slack.channel_id') + self.botid = self.get_secret('app.contact.slack.bot_id') loop = asyncio.get_event_loop() loop.create_task(self.slack_operation_loop()) diff --git a/app/utility/base_world.py b/app/utility/base_world.py index f86de97c2..ac0f230ff 100644 --- a/app/utility/base_world.py +++ b/app/utility/base_world.py @@ -1,4 +1,5 @@ import binascii +import os import string import re import yaml @@ -46,6 +47,26 @@ def set_config(name, prop, value): logging.debug('Configuration (%s) update, setting %s=%s' % (name, prop, value)) BaseWorld._app_configuration[name][prop] = value + @staticmethod + def get_secret(key, env_var=None): + """Retrieve a secret value, checking environment variables first, then config. + + Args: + key: The config key to look up. + env_var: Optional environment variable name to check first. + + Returns: + The secret value from env var or config, or None if neither found. + """ + if env_var: + env_value = os.environ.get(env_var) + if env_value: + return env_value + try: + return BaseWorld.get_config(key) + except KeyError: + return None + @staticmethod def decode_bytes(s, strip_newlines=True): decoded = b64decode(s).decode('utf-8', errors='ignore') diff --git a/conf/default.yml b/conf/default.yml index ba0653c94..a4e31af99 100644 --- a/conf/default.yml +++ b/conf/default.yml @@ -3,9 +3,11 @@ api_key_blue: BLUEADMIN123 api_key_red: ADMIN123 app.contact.dns.domain: mycaldera.caldera app.contact.dns.socket: 0.0.0.0:8853 +# Override with env var CALDERA_GIST_TOKEN app.contact.gist: API_KEY app.contact.html: /weather app.contact.http: http://0.0.0.0:8888 +# Override with env var CALDERA_SLACK_API_KEY app.contact.slack.api_key: SLACK_TOKEN app.contact.slack.bot_id: SLACK_BOT_ID app.contact.slack.channel_id: SLACK_CHANNEL_ID diff --git a/tests/test_get_secret.py b/tests/test_get_secret.py new file mode 100644 index 000000000..4d753e8e1 --- /dev/null +++ b/tests/test_get_secret.py @@ -0,0 +1,36 @@ +import os +import pytest +from app.utility.base_world import BaseWorld + + +@pytest.fixture +def setup_config(): + BaseWorld.apply_config('main', {'app.contact.gist': 'config_token', 'app.contact.slack.api_key': 'config_slack'}) + yield + BaseWorld.clear_config() + + +class TestGetSecret: + def test_env_var_takes_priority(self, setup_config): + os.environ['CALDERA_GIST_TOKEN'] = 'env_token' + try: + result = BaseWorld.get_secret('app.contact.gist', env_var='CALDERA_GIST_TOKEN') + assert result == 'env_token' + finally: + del os.environ['CALDERA_GIST_TOKEN'] + + def test_falls_back_to_config(self, setup_config): + result = BaseWorld.get_secret('app.contact.gist') + assert result == 'config_token' + + def test_returns_none_when_neither(self, setup_config): + result = BaseWorld.get_secret('nonexistent.key', env_var='NONEXISTENT_ENV_VAR') + assert result is None + + def test_env_var_empty_string_falls_back(self, setup_config): + os.environ['CALDERA_GIST_TOKEN'] = '' + try: + result = BaseWorld.get_secret('app.contact.gist', env_var='CALDERA_GIST_TOKEN') + assert result == 'config_token' + finally: + del os.environ['CALDERA_GIST_TOKEN']