diff --git a/CHANGES.rst b/CHANGES.rst index e8e4ee2c..8135a544 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,7 @@ New Features - Add CSS variables for better organization and flexibility (`#1001`_, pkvach) - Add support for comment search by Thread URL in admin interface (`#1020`_, pkvach) - Add sorting option for comments (`#1005`_, pkvach) +- Add support for environment variables in config (`#1037`_, pkvach) .. _#966: https://github.com/posativ/isso/pull/966 .. _#998: https://github.com/isso-comments/isso/pull/998 @@ -20,6 +21,7 @@ New Features .. _#1001: https://github.com/isso-comments/isso/pull/1001 .. _#1020: https://github.com/isso-comments/isso/pull/1020 .. _#1005: https://github.com/isso-comments/isso/pull/1005 +.. _#1037: https://github.com/isso-comments/isso/pull/1037 Breaking Changes ^^^^^^^^^^^^^^^^ diff --git a/docs/docs/reference/server-config.rst b/docs/docs/reference/server-config.rst index ea3c91d8..bf4d0f08 100644 --- a/docs/docs/reference/server-config.rst +++ b/docs/docs/reference/server-config.rst @@ -572,3 +572,24 @@ Booleans ``on``/``off`` etc. .. _getboolean: https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.getboolean + +Environment Variables +^^^^^^^^^^^^^^^^^^^^ +.. versionadded:: 0.13.1 + +You can use environment variables in the configuration file. This is useful for keeping sensitive information, such as passwords, out of the configuration file itself. Environment variables are referenced using the ``$VAR_NAME`` or ``${VAR_NAME}`` syntax. + +Example: + +.. code-block:: ini + :caption: ``isso.cfg`` + + [general] + dbpath = /var/lib/isso/comments.db + host = https://example.tld/ + + [smtp] + username = $SMTP_USERNAME + password = ${SMTP_PASSWORD} + +In this example, the values for ``username`` and ``password`` will be taken from the environment variables ``SMTP_USERNAME`` and ``SMTP_PASSWORD``, respectively. diff --git a/isso/config.py b/isso/config.py index d0572cf6..65bef1ba 100644 --- a/isso/config.py +++ b/isso/config.py @@ -2,6 +2,7 @@ import datetime import logging +import os import pkg_resources import re @@ -82,6 +83,7 @@ class IssoParser(ConfigParser): * Parse human-readable timedelta such as "15m" as "15 minutes" * Handle indented lines as "lists" * Disable string interpolation ('%s') for values + * Support environment variable substitution """ def __init__(self, *args, **kwargs): @@ -95,6 +97,10 @@ def __init__(self, *args, **kwargs): return super(IssoParser, self).__init__( allow_no_value=True, interpolation=None, *args, **kwargs) + def get(self, section, key, **kwargs): + value = super(IssoParser, self).get(section, key, **kwargs) + return os.path.expandvars(value) + def getint(self, section, key): try: delta = timedelta(self.get(section, key)) diff --git a/isso/tests/test_config.py b/isso/tests/test_config.py index 8a15d144..ba691e12 100644 --- a/isso/tests/test_config.py +++ b/isso/tests/test_config.py @@ -2,6 +2,7 @@ import unittest import io +import os from isso import config @@ -34,3 +35,51 @@ def test_parser(self): # Section.get() should function the same way as plain IssoParser foosection = parser.section("foo") self.assertEqual(foosection.get("password"), '%s%%foo') + + def test_parser_with_environment_variables(self): + + parser = config.IssoParser() + parser.read_file(io.StringIO(""" + [foo] + bar = $TEST_ENV_VAR + baz = ${TEST_ENV_VAR2} + """)) + + # Set environment variables + os.environ['TEST_ENV_VAR'] = 'test_value' + os.environ['TEST_ENV_VAR2'] = 'another_test_value' + + # Test environment variable expansion + self.assertEqual(parser.get("foo", "bar"), 'test_value') + self.assertEqual(parser.get("foo", "baz"), 'another_test_value') + + # Test Section.get() with environment variables + foosection = parser.section("foo") + self.assertEqual(foosection.get("bar"), 'test_value') + self.assertEqual(foosection.get("baz"), 'another_test_value') + + # Clean up environment variables + del os.environ['TEST_ENV_VAR'] + del os.environ['TEST_ENV_VAR2'] + + def test_parser_with_missing_environment_variables(self): + + parser = config.IssoParser() + parser.read_file(io.StringIO(""" + [foo] + bar = $MISSING_ENV_VAR + """)) + + self.assertEqual(parser.get("foo", "bar"), '$MISSING_ENV_VAR') + + def test_parser_with_special_characters_in_environment_variables(self): + + os.environ['SPECIAL_ENV_VAR'] = 'value_with_$pecial_characters' + parser = config.IssoParser() + parser.read_file(io.StringIO(""" + [foo] + bar = $SPECIAL_ENV_VAR + """)) + + self.assertEqual(parser.get("foo", "bar"), 'value_with_$pecial_characters') + del os.environ['SPECIAL_ENV_VAR']