Skip to content

Commit f838bb1

Browse files
author
Joachim Jablon
authored
Merge pull request #178 from peopledoc/crash-176
2 parents d4dca4c + 86c12ca commit f838bb1

File tree

10 files changed

+199
-84
lines changed

10 files changed

+199
-84
lines changed

docs/howto/environment.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,19 @@ Your call would look like:
114114
.. code:: console
115115
116116
$ vault-cli env --omit-single-key --path myapp -- myapp
117+
118+
Ignoring errors
119+
---------------
120+
121+
By default, ``vault-cli`` will not launch you program if an error happens during secrets
122+
collection. You can pass ``--force`` to ensure that your program will be launched,
123+
even if it will be missing some secrets.
124+
125+
.. code:: console
126+
127+
$ vault-cli env --path myapp --force -- myapp
128+
129+
.. warning::
130+
131+
Even if just a single key for a secret produces an error (e.g. a template rendering
132+
error), the whole secret will be missing.

tests/unit/test_cli.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,36 @@ def test_env(cli_runner, vault_with_token, mocker):
304304
assert kwargs["environment"]["FOO_BAZ_VALUE"] == "yo"
305305

306306

307+
def test_env_error(cli_runner, vault_with_token, mocker):
308+
exec_command = mocker.patch("vault_cli.environment.exec_command")
309+
310+
vault_with_token.forbidden_get_paths.add("foo")
311+
312+
cli_runner.invoke(cli.cli, ["env", "--path", "foo", "--", "echo", "yay"])
313+
314+
exec_command.assert_not_called()
315+
316+
317+
def test_env_error_force_sub_error(cli_runner, vault_with_token, mocker):
318+
exec_command = mocker.patch("vault_cli.environment.exec_command")
319+
320+
vault_with_token.forbidden_get_paths.add("foo")
321+
322+
cli_runner.invoke(cli.cli, ["env", "--path", "foo", "--force", "--", "echo", "yay"])
323+
324+
exec_command.assert_called()
325+
326+
327+
def test_env_error_force_main_error(cli_runner, vault_with_token, mocker):
328+
exec_command = mocker.patch("vault_cli.environment.exec_command")
329+
330+
vault_with_token.forbidden_list_paths.add("foo")
331+
332+
cli_runner.invoke(cli.cli, ["env", "--path", "foo", "--force", "--", "echo", "yay"])
333+
334+
exec_command.assert_called()
335+
336+
307337
def test_env_prefix(cli_runner, vault_with_token, mocker):
308338
exec_command = mocker.patch("vault_cli.environment.exec_command")
309339

@@ -714,3 +744,12 @@ def test_parse_octal(input, output):
714744
)
715745
def test_repr_octal(input, output):
716746
assert cli.repr_octal(input) == output
747+
748+
749+
def test_ensure_str():
750+
assert cli.ensure_str("foo", "bar") == "foo"
751+
752+
753+
def test_ensure_str_wrong():
754+
with pytest.raises(exceptions.VaultWrongType):
755+
cli.ensure_str(1, "bar")

tests/unit/test_client_base.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,10 @@ def test_vault_client_base_render_template(vault):
399399
assert vault.render_template("Hello {{ vault('a/b').value }}") == "Hello c"
400400

401401

402-
def test_vault_client_base_render_template_path_not_found(vault):
402+
@pytest.mark.parametrize("template", ["Hello {{ vault('a/b') }}", "Hello {{"])
403+
def test_vault_client_base_render_template_path_not_found(vault, template):
403404
with pytest.raises(exceptions.VaultRenderTemplateError):
404-
vault.render_template("Hello {{ vault('a/b') }}")
405+
vault.render_template(template)
405406

406407

407408
@pytest.mark.parametrize(
@@ -499,18 +500,38 @@ def test_vault_client_base_get_secret_missing_key(vault):
499500
vault.get_secret("a", key="username")
500501

501502

503+
def test_vault_client_base_get_secret_template_error(vault, caplog):
504+
vault.db = {"a": {"key": "!template!{{"}}
505+
506+
with pytest.raises(exceptions.VaultRenderTemplateError) as exc_info:
507+
vault.get_secret("a")
508+
509+
assert str(exc_info.value) == 'Error while rendering secret at path "a"'
510+
assert (
511+
str(exc_info.value.__cause__)
512+
== 'Error while rendering secret value for key "key"'
513+
)
514+
assert str(exc_info.value.__cause__.__cause__) == "Jinja2 template syntax error"
515+
516+
502517
def test_vault_client_base_lookup_token(vault):
503518
assert vault.lookup_token() == {"data": {"expire_time": "2100-01-01T00:00:00"}}
504519

505520

506-
def test_vault_client_base_get_secrets_error(vault):
521+
def test_vault_client_base_get_secrets_error(vault, caplog):
507522
vault.db = {"a": {"value": "b"}, "c": {"value": "d"}}
508523
vault.forbidden_get_paths = {"c"}
509524

510525
assert vault.get_secrets("") == {
511526
"a": {"value": "b"},
512-
"c": {"error": "<error while retrieving secret>"},
527+
"c": {},
513528
}
529+
assert caplog.record_tuples[0] == (
530+
"vault_cli.client",
531+
40,
532+
"VaultForbidden: Insufficient access for interacting with the requested "
533+
"secret",
534+
)
514535

515536

516537
def test_vault_client_base_get_secrets_list_forbidden(vault):
@@ -571,12 +592,12 @@ def test_vault_client_base_base_path(vault, path, expected):
571592
assert vault.base_path == expected
572593

573594

574-
def test_vault_client_base_get_secret_implicit_cache_ends(vault):
595+
def test_vault_client_base_get_secret_implicit_cache(vault):
575596
vault.db = {"a": {"value": "b"}}
576597
assert vault.get_secret("a") == {"value": "b"}
577598
vault.db = {"a": {"value": "c"}}
578-
# Value updated. Cache was just for the duration of the call
579-
assert vault.get_secret("a") == {"value": "c"}
599+
# Value was cached
600+
assert vault.get_secret("a") == {"value": "b"}
580601

581602

582603
class RaceConditionTestVaultClient(testing.TestVaultClient):
@@ -600,13 +621,16 @@ def test_vault_client_base_get_secret_implicit_cache_no_race_condition():
600621

601622
vault = RaceConditionTestVaultClient()
602623

603-
assert vault.get_secret("a") == {"b": "b0", "c": "c0"}
604-
assert vault.get_secret("a") == {"b": "b1", "c": "c1"}
624+
with vault:
625+
assert vault.get_secret("a") == {"b": "b0", "c": "c0"}
626+
with vault:
627+
assert vault.get_secret("a") == {"b": "b1", "c": "c1"}
605628

606629
vault.db = {"d": {"value": """!template!{{ vault("a").b }}-{{ vault("a").c }}"""}}
607630

608631
# b2-c3 would be the value if caching didn't work.
609-
assert vault.get_secret("d") == {"value": "b2-c2"}
632+
with vault:
633+
assert vault.get_secret("d") == {"value": "b2-c2"}
610634

611635

612636
def test_vault_client_base_get_secrets_implicit_cache_no_race_condition():
@@ -628,8 +652,9 @@ def test_vault_client_base_get_secrets_implicit_cache_no_race_condition():
628652

629653
def test_vault_client_base_get_secret_explicit_cache(vault):
630654
vault.db = {"a": {"value": "b"}}
631-
with vault.caching():
655+
with vault:
632656
assert vault.get_secret("a") == {"value": "b"}
633657
vault.db = {"a": {"value": "c"}}
634658
# Value not updated
635659
assert vault.get_secret("a") == {"value": "b"}
660+
assert vault.get_secret("a") == {"value": "c"}

tests/unit/test_client_hvac.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import hvac
44
import pytest
5+
import requests
56

67
from vault_cli import client, exceptions
78

@@ -176,6 +177,7 @@ def test_set_context_manager(mocker):
176177
(hvac.exceptions.InternalServerError, exceptions.VaultInternalServerError),
177178
(hvac.exceptions.VaultDown, exceptions.VaultSealed),
178179
(hvac.exceptions.UnexpectedError, exceptions.VaultAPIException),
180+
(requests.exceptions.ConnectionError, exceptions.VaultConnectionError),
179181
],
180182
)
181183
def test_handle_errors(hvac_exc, vault_cli_exc):

tests/unit/test_exceptions.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ def test_vault_overwrite_secret_error():
1111

1212

1313
def test_vault_render_template_error():
14-
assert (
15-
str(exceptions.VaultRenderTemplateError("yay"))
16-
== "VaultRenderTemplateError: Error while rendering template: yay"
17-
)
14+
assert str(exceptions.VaultRenderTemplateError("yay")) == "yay"
1815

1916

2017
@pytest.mark.parametrize(

vault_cli/cli.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import yaml
2020

2121
import vault_cli
22-
from vault_cli import client, environment, exceptions, settings, ssh, types
22+
from vault_cli import client, environment, exceptions, settings, ssh, types, utils
2323

2424
logger = logging.getLogger(__name__)
2525

@@ -59,14 +59,7 @@ def handle_errors():
5959
try:
6060
yield
6161
except exceptions.VaultException as exc:
62-
messages = []
63-
while True:
64-
exc_str = str(exc).strip()
65-
messages.append(f"{type(exc).__name__}: {exc_str}")
66-
exc = exc.__cause__ or exc.__context__
67-
if not exc:
68-
break
69-
raise click.ClickException("\n".join(messages))
62+
raise click.ClickException("\n".join(utils.extract_error_messages(exc)))
7063

7164

7265
def print_version(ctx, __, value):
@@ -417,17 +410,23 @@ def delete(client_obj: client.VaultClientBase, name: str, key: Optional[str]) ->
417410
@click.option(
418411
"-o",
419412
"--omit-single-key/--no-omit-single-key",
420-
is_flag=True,
421413
default=False,
422414
help="When the secret has only one key, don't use that key to build the name of the environment variable",
423415
)
416+
@click.option(
417+
"-f",
418+
"--force/--no-force",
419+
default=False,
420+
help="Run the command even if there is a problem while reading secrets",
421+
)
424422
@click.argument("command", nargs=-1, required=True)
425423
@click.pass_obj
426424
@handle_errors()
427425
def env(
428426
client_obj: client.VaultClientBase,
429427
path: Sequence[str],
430428
omit_single_key: bool,
429+
force: bool,
431430
command: Sequence[str],
432431
) -> NoReturn:
433432
"""
@@ -462,15 +461,19 @@ def env(
462461
path_with_key, _, prefix = path.partition("=")
463462
path, _, filter_key = path_with_key.partition(":")
464463

464+
env_updates = {}
465465
env_updates = environment.get_envvars(
466466
vault_client=client_obj,
467467
path=path,
468468
prefix=prefix,
469469
omit_single_key=omit_single_key,
470470
filter_key=filter_key,
471471
)
472+
472473
env_secrets.update(env_updates)
473474

475+
if bool(client_obj.errors) and not force:
476+
raise click.ClickException("There was an error while reading the secrets.")
474477
environment.exec_command(command=command, environment=env_secrets)
475478

476479

@@ -639,20 +642,30 @@ def ssh_(
639642
if ":" not in key:
640643
raise click.UsageError("Format for private_key: `path/to/private_key:key`")
641644
private_key_name, private_key_key = key.rsplit(":", 1)
642-
private_key_secret = client_obj.get_secret(private_key_name, private_key_key)
645+
private_key_secret_obj = client_obj.get_secret(private_key_name, private_key_key)
646+
private_key_secret = ensure_str(secret=private_key_secret_obj, path=key)
643647

644648
passphrase_secret = None
645649
if passphrase:
646650
if ":" not in passphrase:
647651
raise click.UsageError("Format for passphrase: `path/to/passphrase:key`")
648652
passphrase_name, passphrase_key = passphrase.rsplit(":", 1)
649-
passphrase_secret = client_obj.get_secret(passphrase_name, passphrase_key)
653+
passphrase_secret_obj = client_obj.get_secret(passphrase_name, passphrase_key)
654+
passphrase_secret = ensure_str(secret=passphrase_secret_obj, path=passphrase)
650655

651656
ssh.add_key(key=private_key_secret, passphrase=passphrase_secret)
652657

653658
environment.exec_command(command=command)
654659

655660

661+
def ensure_str(secret, path) -> str:
662+
if not isinstance(secret, str):
663+
raise exceptions.VaultWrongType(
664+
f"secret at {path} is not a string but {type(secret)}"
665+
)
666+
return secret
667+
668+
656669
# This is purposedly not called as a click subcommand, and we cannot take any argument
657670
def askpass():
658671
print(os.environ[ssh.SSH_PASSPHRASE_ENVVAR])

0 commit comments

Comments
 (0)