mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00
Fix utf8 handling in 'pass' renderer and make it more robust
This commit is contained in:
parent
098dae15cb
commit
8dfc923876
3 changed files with 102 additions and 6 deletions
1
changelog/64300.fixed.md
Normal file
1
changelog/64300.fixed.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix utf8 handling in 'pass' renderer
|
|
@ -145,20 +145,16 @@ def _fetch_secret(pass_path):
|
||||||
env["GNUPGHOME"] = pass_gnupghome
|
env["GNUPGHOME"] = pass_gnupghome
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env)
|
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env, encoding="utf-8")
|
||||||
pass_data, pass_error = proc.communicate()
|
pass_data, pass_error = proc.communicate()
|
||||||
pass_returncode = proc.returncode
|
pass_returncode = proc.returncode
|
||||||
except OSError as e:
|
except (OSError, UnicodeDecodeError) as e:
|
||||||
pass_data, pass_error = "", str(e)
|
pass_data, pass_error = "", str(e)
|
||||||
pass_returncode = 1
|
pass_returncode = 1
|
||||||
|
|
||||||
# The version of pass used during development sent output to
|
# The version of pass used during development sent output to
|
||||||
# stdout instead of stderr even though its returncode was non zero.
|
# stdout instead of stderr even though its returncode was non zero.
|
||||||
if pass_returncode or not pass_data:
|
if pass_returncode or not pass_data:
|
||||||
try:
|
|
||||||
pass_error = pass_error.decode("utf-8")
|
|
||||||
except (AttributeError, ValueError):
|
|
||||||
pass
|
|
||||||
msg = f"Could not fetch secret '{pass_path}' from the password store: {pass_error}"
|
msg = f"Could not fetch secret '{pass_path}' from the password store: {pass_error}"
|
||||||
if pass_strict_fetch:
|
if pass_strict_fetch:
|
||||||
raise SaltRenderError(msg)
|
raise SaltRenderError(msg)
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import importlib
|
import importlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import salt.exceptions
|
import salt.exceptions
|
||||||
|
import salt.utils.files
|
||||||
from tests.support.mock import MagicMock, patch
|
from tests.support.mock import MagicMock, patch
|
||||||
|
|
||||||
# "pass" is a reserved keyword, we need to import it differently
|
# "pass" is a reserved keyword, we need to import it differently
|
||||||
|
@ -19,6 +23,47 @@ def configure_loader_modules(master_opts):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def pass_executable(request):
|
||||||
|
tmp_dir = tempfile.mkdtemp(prefix="salt_pass_")
|
||||||
|
pass_path = os.path.join(tmp_dir, "pass")
|
||||||
|
with salt.utils.files.fopen(pass_path, "w") as f:
|
||||||
|
f.write("#!/bin/sh\n")
|
||||||
|
# return path path wrapped into unicode characters
|
||||||
|
# pass args ($1, $2) are ("show", <pass_path>)
|
||||||
|
f.write('echo "α>>> $2 <<<β"\n')
|
||||||
|
os.chmod(pass_path, 0o755)
|
||||||
|
yield pass_path
|
||||||
|
shutil.rmtree(tmp_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def pass_executable_error(request):
|
||||||
|
tmp_dir = tempfile.mkdtemp(prefix="salt_pass_")
|
||||||
|
pass_path = os.path.join(tmp_dir, "pass")
|
||||||
|
with salt.utils.files.fopen(pass_path, "w") as f:
|
||||||
|
f.write("#!/bin/sh\n")
|
||||||
|
# return error message with unicode characters
|
||||||
|
f.write('echo "ERROR: αβγ" >&2\n')
|
||||||
|
f.write("exit 1\n")
|
||||||
|
os.chmod(pass_path, 0o755)
|
||||||
|
yield pass_path
|
||||||
|
shutil.rmtree(tmp_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def pass_executable_invalid_utf8(request):
|
||||||
|
tmp_dir = tempfile.mkdtemp(prefix="salt_pass_")
|
||||||
|
pass_path = os.path.join(tmp_dir, "pass")
|
||||||
|
with salt.utils.files.fopen(pass_path, "wb") as f:
|
||||||
|
f.write(b"#!/bin/sh\n")
|
||||||
|
# return invalid utf-8 sequence
|
||||||
|
f.write(b'echo "\x80\x81"\n')
|
||||||
|
os.chmod(pass_path, 0o755)
|
||||||
|
yield pass_path
|
||||||
|
shutil.rmtree(tmp_dir)
|
||||||
|
|
||||||
|
|
||||||
# The default behavior is that if fetching a secret from pass fails,
|
# The default behavior is that if fetching a secret from pass fails,
|
||||||
# the value is passed through. Even the trailing newlines are preserved.
|
# the value is passed through. Even the trailing newlines are preserved.
|
||||||
def test_passthrough():
|
def test_passthrough():
|
||||||
|
@ -161,3 +206,57 @@ def test_env():
|
||||||
call_args, call_kwargs = popen_mock.call_args_list[0]
|
call_args, call_kwargs = popen_mock.call_args_list[0]
|
||||||
assert call_kwargs["env"]["GNUPGHOME"] == config["pass_gnupghome"]
|
assert call_kwargs["env"]["GNUPGHOME"] == config["pass_gnupghome"]
|
||||||
assert call_kwargs["env"]["PASSWORD_STORE_DIR"] == config["pass_dir"]
|
assert call_kwargs["env"]["PASSWORD_STORE_DIR"] == config["pass_dir"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_on_windows(reason="Not supported on Windows")
|
||||||
|
def test_utf8(pass_executable):
|
||||||
|
config = {
|
||||||
|
"pass_variable_prefix": "pass:",
|
||||||
|
"pass_strict_fetch": True,
|
||||||
|
}
|
||||||
|
mocks = {
|
||||||
|
"_get_pass_exec": MagicMock(return_value=pass_executable),
|
||||||
|
}
|
||||||
|
|
||||||
|
pass_path = "pass:secret"
|
||||||
|
with patch.dict(pass_.__opts__, config), patch.dict(pass_.__dict__, mocks):
|
||||||
|
result = pass_.render(pass_path)
|
||||||
|
assert result == "α>>> secret <<<β"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_on_windows(reason="Not supported on Windows")
|
||||||
|
def test_utf8_error(pass_executable_error):
|
||||||
|
config = {
|
||||||
|
"pass_variable_prefix": "pass:",
|
||||||
|
"pass_strict_fetch": True,
|
||||||
|
}
|
||||||
|
mocks = {
|
||||||
|
"_get_pass_exec": MagicMock(return_value=pass_executable_error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pass_path = "pass:secret"
|
||||||
|
with patch.dict(pass_.__opts__, config), patch.dict(pass_.__dict__, mocks):
|
||||||
|
with pytest.raises(
|
||||||
|
salt.exceptions.SaltRenderError,
|
||||||
|
match=r"Could not fetch secret 'secret' from the password store: ERROR: αβγ",
|
||||||
|
):
|
||||||
|
result = pass_.render(pass_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip_on_windows(reason="Not supported on Windows")
|
||||||
|
def test_invalid_utf8(pass_executable_invalid_utf8):
|
||||||
|
config = {
|
||||||
|
"pass_variable_prefix": "pass:",
|
||||||
|
"pass_strict_fetch": True,
|
||||||
|
}
|
||||||
|
mocks = {
|
||||||
|
"_get_pass_exec": MagicMock(return_value=pass_executable_invalid_utf8),
|
||||||
|
}
|
||||||
|
|
||||||
|
pass_path = "pass:secret"
|
||||||
|
with patch.dict(pass_.__opts__, config), patch.dict(pass_.__dict__, mocks):
|
||||||
|
with pytest.raises(
|
||||||
|
salt.exceptions.SaltRenderError,
|
||||||
|
match=r"Could not fetch secret 'secret' from the password store: 'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte",
|
||||||
|
):
|
||||||
|
result = pass_.render(pass_path)
|
||||||
|
|
Loading…
Add table
Reference in a new issue