@@ -48,7 +48,7 @@ example: | |||
| 48 | 48 | ||
| 49 | 49 | [elsewhere] | |
| 50 | 50 | url = http://else.whe.re:8080 | |
| 51 | - private_token = lookup: pass show path/to/password | head -n1 | ||
| 51 | + private_token = helper: path/to/helper.sh | ||
| 52 | 52 | timeout = 1 | |
| 53 | 53 | ||
| 54 | 54 | The ``default`` option of the ``[global]`` section defines the GitLab server to | |
@@ -119,6 +119,27 @@ server, with very limited permissions. | |||
| 119 | 119 | * - ``http_password`` | |
| 120 | 120 | - Password for optional HTTP authentication | |
| 121 | 121 | ||
| 122 | + For all settings, which contain secrets (``http_password``, | ||
| 123 | + ``personal_token``, ``oauth_token``, ``job_token``), you can specify | ||
| 124 | + a helper program to retrieve the secret indicated by ``helper:`` | ||
| 125 | + prefix. You can only specify a path to a program without any | ||
| 126 | + parameters. It is expected, that the program prints the secret to | ||
| 127 | + standard output. | ||
| 128 | + | ||
| 129 | + Example for a `keyring <https://github.com/jaraco/keyring>`_ helper: | ||
| 130 | + | ||
| 131 | + .. code-block:: bash | ||
| 132 | + | ||
| 133 | + #!/bin/bash | ||
| 134 | + keyring get Service Username | ||
| 135 | + | ||
| 136 | + Example for a `pass <https://www.passwordstore.org>`_ helper: | ||
| 137 | + | ||
| 138 | + .. code-block:: bash | ||
| 139 | + | ||
| 140 | + #!/bin/bash | ||
| 141 | + pass show path/to/password | head -n 1 | ||
| 142 | + | ||
| 122 | 143 | CLI | |
| 123 | 144 | === | |
| 124 | 145 | ||
@@ -34,6 +34,11 @@ def _env_config() -> List[str]: | |||
| 34 | 34 | os.path.expanduser("~/.python-gitlab.cfg"), | |
| 35 | 35 | ] | |
| 36 | 36 | ||
| 37 | + HELPER_PREFIX = "helper:" | ||
| 38 | + | ||
| 39 | + HELPER_ATTRIBUTES = [ | ||
| 40 | + "job_token", "http_password", "private_token", "oauth_token" | ||
| 41 | + ] | ||
| 37 | 42 | ||
| 38 | 43 | class ConfigError(Exception): | |
| 39 | 44 | pass | |
@@ -151,15 +156,7 @@ def __init__( | |||
| 151 | 156 | except Exception: | |
| 152 | 157 | pass | |
| 153 | 158 | ||
| 154 | - for attr in ("job_token", "http_password", "private_token", "oauth_token"): | ||
| 155 | - value = getattr(self, attr) | ||
| 156 | - prefix = "lookup:" | ||
| 157 | - if isinstance(value, str) and value.lower().strip().startswith(prefix): | ||
| 158 | - helper = value[len(prefix) :].strip() | ||
| 159 | - value = ( | ||
| 160 | - subprocess.check_output(helper, shell=True).decode("utf-8").strip() | ||
| 161 | - ) | ||
| 162 | - setattr(self, attr, value) | ||
| 159 | + self._get_values_from_helper() | ||
| 163 | 160 | ||
| 164 | 161 | self.api_version = "4" | |
| 165 | 162 | try: | |
@@ -203,3 +200,13 @@ def __init__( | |||
| 203 | 200 | self.user_agent = self._config.get(self.gitlab_id, "user_agent") | |
| 204 | 201 | except Exception: | |
| 205 | 202 | pass | |
| 203 | + | ||
| 204 | + def _get_values_from_helper(self): | ||
| 205 | + """Update attributes, which may get values from an external helper program | ||
| 206 | + """ | ||
| 207 | + for attr in HELPER_ATTRIBUTES: | ||
| 208 | + value = getattr(self, attr) | ||
| 209 | + if isinstance(value, str) and value.lower().strip().startswith(HELPER_PREFIX): | ||
| 210 | + helper = value[len(HELPER_PREFIX) :].strip() | ||
| 211 | + value = subprocess.check_output([helper]).decode("utf-8").strip() | ||
| 212 | + setattr(self, attr, value) | ||
@@ -17,6 +17,7 @@ | |||
| 17 | 17 | ||
| 18 | 18 | import os | |
| 19 | 19 | import unittest | |
| 20 | + from textwrap import dedent | ||
| 20 | 21 | ||
| 21 | 22 | import mock | |
| 22 | 23 | import io | |
@@ -51,10 +52,6 @@ | |||
| 51 | 52 | [four] | |
| 52 | 53 | url = https://four.url | |
| 53 | 54 | oauth_token = STUV | |
| 54 | - | ||
| 55 | - [five] | ||
| 56 | - url = https://five.url | ||
| 57 | - oauth_token = lookup: echo "foobar" | ||
| 58 | 55 | """ | |
| 59 | 56 | ||
| 60 | 57 | custom_user_agent_config = """[global] | |
@@ -196,16 +193,33 @@ def test_valid_data(m_open, path_exists): | |||
| 196 | 193 | assert 2 == cp.timeout | |
| 197 | 194 | assert True == cp.ssl_verify | |
| 198 | 195 | ||
| 199 | - fd = io.StringIO(valid_config) | ||
| 196 | + | ||
| 197 | + @mock.patch("os.path.exists") | ||
| 198 | + @mock.patch("builtins.open") | ||
| 199 | + def test_data_from_helper(m_open, path_exists, tmp_path): | ||
| 200 | + helper = (tmp_path / "helper.sh") | ||
| 201 | + helper.write_text(dedent("""\ | ||
| 202 | + #!/bin/sh | ||
| 203 | + echo "secret" | ||
| 204 | + """)) | ||
| 205 | + helper.chmod(0o755) | ||
| 206 | + | ||
| 207 | + fd = io.StringIO(dedent("""\ | ||
| 208 | + [global] | ||
| 209 | + default = helper | ||
| 210 | + | ||
| 211 | + [helper] | ||
| 212 | + url = https://helper.url | ||
| 213 | + oauth_token = helper: %s | ||
| 214 | + """) % helper) | ||
| 215 | + | ||
| 200 | 216 | fd.close = mock.Mock(return_value=None) | |
| 201 | 217 | m_open.return_value = fd | |
| 202 | - cp = config.GitlabConfigParser(gitlab_id="five") | ||
| 203 | - assert "five" == cp.gitlab_id | ||
| 204 | - assert "https://five.url" == cp.url | ||
| 218 | + cp = config.GitlabConfigParser(gitlab_id="helper") | ||
| 219 | + assert "helper" == cp.gitlab_id | ||
| 220 | + assert "https://helper.url" == cp.url | ||
| 205 | 221 | assert None == cp.private_token | |
| 206 | - assert "foobar" == cp.oauth_token | ||
| 207 | - assert 2 == cp.timeout | ||
| 208 | - assert True == cp.ssl_verify | ||
| 222 | + assert "secret" == cp.oauth_token | ||
| 209 | 223 | ||
| 210 | 224 | ||
| 211 | 225 | @mock.patch("os.path.exists") | |
0 commit comments