diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 80806c6e..06ac9cf9 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -642,16 +642,43 @@ def read_config(): return config +def expand_env_dict(s): + """Expand env var dict to a string value. + + {env: varName} syntax can be used to replace any string value in the + config with the value of an environment variable "varName". This + allows for secrets management when commiting the config file to a + public git repo. + + """ + if not s or type(s) not in (str, dict): + return + if isinstance(s, dict): + if 'env' not in s or len(s) > 1: + raise TypeError(_('Only accepts a single key "env"')) + var = s['env'] + s = os.getenv(var) + if not s: + logging.error( + _('Environment variable {{env: {var}}} is not set!').format(var=var) + ) + return + return os.path.expanduser(s) + + def parse_mirrors_config(mirrors): """Mirrors can be specified as a string, list of strings, or dictionary map.""" if isinstance(mirrors, str): - return [{"url": mirrors}] - elif all(isinstance(item, str) for item in mirrors): - return [{'url': i} for i in mirrors] - elif all(isinstance(item, dict) for item in mirrors): + return [{"url": expand_env_dict(mirrors)}] + if isinstance(mirrors, dict): + return [{"url": expand_env_dict(mirrors)}] + if all(isinstance(item, str) for item in mirrors): + return [{'url': expand_env_dict(i)} for i in mirrors] + if all(isinstance(item, dict) for item in mirrors): + for item in mirrors: + item['url'] = expand_env_dict(item['url']) return mirrors - else: - raise TypeError(_('only accepts strings, lists, and tuples')) + raise TypeError(_('only accepts strings, lists, and tuples')) def get_mirrors(url, filename=None): diff --git a/tests/test_common.py b/tests/test_common.py index 3513bf53..bbdaa016 100755 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2122,6 +2122,27 @@ class CommonTest(unittest.TestCase): ) fdroidserver.common.read_config() + @mock.patch.dict(os.environ, {'PATH': os.getenv('PATH')}, clear=True) + def test_config_with_env_string(self): + """Test whether env works in keys with string values.""" + os.chdir(self.testdir) + testvalue = 'this is just a test' + Path('config.yml').write_text('keypass: {env: foo}') + os.environ['foo'] = testvalue + self.assertEqual(testvalue, fdroidserver.common.get_config()['keypass']) + + @mock.patch.dict(os.environ, {'PATH': os.getenv('PATH')}, clear=True) + def test_config_with_env_path(self): + """Test whether env works in keys with path values.""" + os.chdir(self.testdir) + path = 'user@server:/path/to/bar/' + os.environ['foo'] = path + Path('config.yml').write_text('serverwebroot: {env: foo}') + self.assertEqual( + [{'url': path}], + fdroidserver.common.get_config()['serverwebroot'], + ) + def test_setup_status_output(self): os.chdir(self.tmpdir) start_timestamp = time.gmtime() @@ -2847,6 +2868,41 @@ class CommonTest(unittest.TestCase): fdroidserver.common.read_config()['serverwebroot'], ) + @mock.patch.dict(os.environ, {'PATH': os.getenv('PATH')}, clear=True) + def test_config_serverwebroot_list_of_dicts_env(self): + os.chdir(self.testdir) + url = 'foo@example.com:/var/www/' + os.environ['serverwebroot'] = url + fdroidserver.common.write_config_file( + textwrap.dedent( + """\ + serverwebroot: + - url: {env: serverwebroot} + index_only: true + """ + ) + ) + self.assertEqual( + [{'url': url, 'index_only': True}], + fdroidserver.common.read_config()['serverwebroot'], + ) + + def test_expand_env_dict_fake_str(self): + testvalue = '"{env: foo}"' + self.assertEqual(testvalue, fdroidserver.common.expand_env_dict(testvalue)) + + @mock.patch.dict(os.environ, {'PATH': os.getenv('PATH')}, clear=True) + def test_expand_env_dict_good(self): + name = 'foo' + value = 'bar' + os.environ[name] = value + self.assertEqual(value, fdroidserver.common.expand_env_dict({'env': name})) + + @mock.patch.dict(os.environ, {'PATH': os.getenv('PATH')}, clear=True) + def test_expand_env_dict_bad_dict(self): + with self.assertRaises(TypeError): + fdroidserver.common.expand_env_dict({'env': 'foo', 'foo': 'bar'}) + def test_parse_mirrors_config_str(self): s = 'foo@example.com:/var/www' mirrors = yaml.load("""'%s'""" % s) @@ -2868,6 +2924,27 @@ class CommonTest(unittest.TestCase): [{'url': s}], fdroidserver.common.parse_mirrors_config(mirrors) ) + @mock.patch.dict(os.environ, {'PATH': os.getenv('PATH'), 'foo': 'bar'}, clear=True) + def test_parse_mirrors_config_env_str(self): + mirrors = yaml.load('{env: foo}') + self.assertEqual( + [{'url': 'bar'}], fdroidserver.common.parse_mirrors_config(mirrors) + ) + + def test_parse_mirrors_config_env_list(self): + s = 'foo@example.com:/var/www' + mirrors = yaml.load("""- '%s'""" % s) + self.assertEqual( + [{'url': s}], fdroidserver.common.parse_mirrors_config(mirrors) + ) + + def test_parse_mirrors_config_env_dict(self): + s = 'foo@example.com:/var/www' + mirrors = yaml.load("""- url: '%s'""" % s) + self.assertEqual( + [{'url': s}], fdroidserver.common.parse_mirrors_config(mirrors) + ) + def test_KnownApks_recordapk(self): """Test that added dates are being fetched from the index. diff --git a/tests/test_lint.py b/tests/test_lint.py index c9e7b3f4..95752cb9 100755 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -550,7 +550,12 @@ class ConfigYmlTest(LintTest): self.config_yml.write_text('sdk_path: /opt/android-sdk\n') self.assertTrue(fdroidserver.lint.lint_config(self.config_yml)) - def test_config_yml_str_dict(self): + def test_config_yml_str_list_of_dicts_env(self): + """serverwebroot can be str, list of str, or list of dicts.""" + self.config_yml.write_text('serverwebroot: {env: ANDROID_HOME}\n') + self.assertTrue(fdroidserver.lint.lint_config(self.config_yml)) + + def test_config_yml_str_env(self): self.config_yml.write_text('sdk_path: {env: ANDROID_HOME}\n') self.assertTrue(fdroidserver.lint.lint_config(self.config_yml))