diff options
-rw-r--r-- | docs/cli-usage.rst | 23 | ||||
-rw-r--r-- | gitlab/config.py | 25 | ||||
-rw-r--r-- | gitlab/tests/test_config.py | 36 |
3 files changed, 63 insertions, 21 deletions
diff --git a/docs/cli-usage.rst b/docs/cli-usage.rst index 71c8577..c27e6c5 100644 --- a/docs/cli-usage.rst +++ b/docs/cli-usage.rst @@ -48,7 +48,7 @@ example: [elsewhere] url = http://else.whe.re:8080 - private_token = lookup: pass show path/to/password | head -n1 + private_token = helper: path/to/helper.sh timeout = 1 The ``default`` option of the ``[global]`` section defines the GitLab server to @@ -119,6 +119,27 @@ server, with very limited permissions. * - ``http_password`` - Password for optional HTTP authentication +For all settings, which contain secrets (``http_password``, +``personal_token``, ``oauth_token``, ``job_token``), you can specify +a helper program to retrieve the secret indicated by ``helper:`` +prefix. You can only specify a path to a program without any +parameters. It is expected, that the program prints the secret to +standard output. + +Example for a `keyring <https://github.com/jaraco/keyring>`_ helper: + +.. code-block:: bash + + #!/bin/bash + keyring get Service Username + +Example for a `pass <https://www.passwordstore.org>`_ helper: + +.. code-block:: bash + + #!/bin/bash + pass show path/to/password | head -n 1 + CLI === diff --git a/gitlab/config.py b/gitlab/config.py index a6f25ac..67f5082 100644 --- a/gitlab/config.py +++ b/gitlab/config.py @@ -34,6 +34,11 @@ _DEFAULT_FILES: List[str] = _env_config() + [ os.path.expanduser("~/.python-gitlab.cfg"), ] +HELPER_PREFIX = "helper:" + +HELPER_ATTRIBUTES = [ + "job_token", "http_password", "private_token", "oauth_token" +] class ConfigError(Exception): pass @@ -151,15 +156,7 @@ class GitlabConfigParser(object): except Exception: pass - for attr in ("job_token", "http_password", "private_token", "oauth_token"): - value = getattr(self, attr) - prefix = "lookup:" - if isinstance(value, str) and value.lower().strip().startswith(prefix): - helper = value[len(prefix) :].strip() - value = ( - subprocess.check_output(helper, shell=True).decode("utf-8").strip() - ) - setattr(self, attr, value) + self._get_values_from_helper() self.api_version = "4" try: @@ -203,3 +200,13 @@ class GitlabConfigParser(object): self.user_agent = self._config.get(self.gitlab_id, "user_agent") except Exception: pass + + def _get_values_from_helper(self): + """Update attributes, which may get values from an external helper program + """ + for attr in HELPER_ATTRIBUTES: + value = getattr(self, attr) + if isinstance(value, str) and value.lower().strip().startswith(HELPER_PREFIX): + helper = value[len(HELPER_PREFIX) :].strip() + value = subprocess.check_output([helper]).decode("utf-8").strip() + setattr(self, attr, value)
\ No newline at end of file diff --git a/gitlab/tests/test_config.py b/gitlab/tests/test_config.py index 644b0c1..60c8853 100644 --- a/gitlab/tests/test_config.py +++ b/gitlab/tests/test_config.py @@ -17,6 +17,7 @@ import os import unittest +from textwrap import dedent import mock import io @@ -51,10 +52,6 @@ per_page = 50 [four] url = https://four.url oauth_token = STUV - -[five] -url = https://five.url -oauth_token = lookup: echo "foobar" """ custom_user_agent_config = """[global] @@ -196,16 +193,33 @@ def test_valid_data(m_open, path_exists): assert 2 == cp.timeout assert True == cp.ssl_verify - fd = io.StringIO(valid_config) + +@mock.patch("os.path.exists") +@mock.patch("builtins.open") +def test_data_from_helper(m_open, path_exists, tmp_path): + helper = (tmp_path / "helper.sh") + helper.write_text(dedent("""\ + #!/bin/sh + echo "secret" + """)) + helper.chmod(0o755) + + fd = io.StringIO(dedent("""\ + [global] + default = helper + + [helper] + url = https://helper.url + oauth_token = helper: %s + """) % helper) + fd.close = mock.Mock(return_value=None) m_open.return_value = fd - cp = config.GitlabConfigParser(gitlab_id="five") - assert "five" == cp.gitlab_id - assert "https://five.url" == cp.url + cp = config.GitlabConfigParser(gitlab_id="helper") + assert "helper" == cp.gitlab_id + assert "https://helper.url" == cp.url assert None == cp.private_token - assert "foobar" == cp.oauth_token - assert 2 == cp.timeout - assert True == cp.ssl_verify + assert "secret" == cp.oauth_token @mock.patch("os.path.exists") |