Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions did/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import locale
import os
import re
import shlex
import subprocess
import sys
from configparser import NoOptionError, NoSectionError
from datetime import timedelta
Expand Down Expand Up @@ -532,19 +534,27 @@ def alias(self, aliases: Optional[str], stats: Optional[str]) -> None:
def get_token(
config: dict[str, str],
token_key: str = "token",
token_file_key: str = "token_file") -> Optional[str]:
token_file_key: str = "token_file",
token_command_key: str = "token_command") -> Optional[str]:
"""
Extract the authentication token from config or token file
Extract the authentication token from config, token file, or command

Returns the contents of `config[token_key]`, or the file contents of
`config[token_file_key]` if no `config[token]` exists. If neither
keys exist, `None` is returned.
Returns the contents of ``config[token_key]``, the file contents of
``config[token_file_key]``, or the stdout of
``config[token_command_key]``. If none of these keys exist,
``None`` is returned.

Sometimes you want to be able to store a token in a file rather than
in the your plain config file. Use this function to support a system
wide mechanism to retrieve tokens or secrets either directly from
the config file as plain text or from an outsourced file.

Use ``token_command`` to run an external command that prints the
token to stdout. This is useful for retrieving tokens from CLI
tools such as ``glab``::

token_command = glab config get --host gitlab.com token

:param config:
A configuration dictionary.
:param token_key:
Expand All @@ -553,6 +563,9 @@ def get_token(
:param token_file_key:
The dict entry to look for when the token is supposed to be read
from file.
:param token_command_key:
The dict entry to look for when the token is retrieved by
running an external command.
:returns:
The stripped token or `None` if no or only empty entries were
found in the `config` dict.
Expand All @@ -566,6 +579,16 @@ def get_token(
file_path = os.path.expanduser(config[token_file_key])
with open(file_path, encoding="utf-8") as token_file:
token = token_file.read().strip()
elif token_command_key in config:
command = config[token_command_key]
try:
result = subprocess.run(
shlex.split(command),
capture_output=True, text=True, check=True)
token = result.stdout.strip()
except (subprocess.CalledProcessError, OSError) as err:
raise ReportError(
f"Token command '{command}' failed: {err}") from err

if token == "":
token = None
Expand Down
7 changes: 7 additions & 0 deletions did/plugins/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@
url = https://gitlab.com/
token = <authentication-token>
token_file = <authentication-token-file>
token_command = <command-to-retrieve-token>
login = <username>
ssl_verify = true

The authentication token is required. Create it in the GitLab web
interface (select ``api`` as the desired scope). See the `GitLab API`__
documentation for details.

Use ``token_command`` to run an external command that prints the
token to stdout. For example, to read tokens from the ``glab`` CLI
configuration::

token_command = glab config get --host gitlab.com token

Use ``login`` to override user name detected from the email address.
See the :doc:`config` documentation for details on using aliases.
Use ``ssl_verify`` to enable/disable SSL verification (default: true)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,26 @@ def test_get_token_file_different_name(self) -> None:
config = {"mytoken_file": filename}
assert did.base.get_token(
config, token_file_key="mytoken_file") == token_in_file

def test_get_token_command(self) -> None:
""" Test getting a token from a command """
token = str(uuid4())
config = {"token_command": f"echo {token}"}
assert did.base.get_token(config) == token

def test_get_token_command_empty(self) -> None:
""" Test getting a token from a command that outputs whitespace """
config = {"token_command": "echo"}
assert did.base.get_token(config) is None

def test_get_token_command_failure(self) -> None:
""" Test getting a token from a command that fails """
config = {"token_command": "false"}
with pytest.raises(did.base.ReportError):
did.base.get_token(config)

def test_get_token_command_precedence(self) -> None:
""" Test that plain token takes precedence over command """
token_plain = str(uuid4())
config = {"token": token_plain, "token_command": "echo other"}
assert did.base.get_token(config) == token_plain