From 7a6202f076993e7f2ab9191af28fad0756d479c0 Mon Sep 17 00:00:00 2001 From: Philippe Muller Date: Tue, 30 Apr 2024 21:08:13 +0800 Subject: [PATCH 1/5] fix: add support for nested pillar data --- CHANGELOG.md | 4 ++++ example/pillar/test.sls | 7 ++++--- example/states/test.sls | 6 +++--- src/saltstack_age/renderers/age.py | 15 ++++++++++----- src/saltstack_age/secure_value.py | 5 +++-- tests/integration/_test_renderer_identity.py | 13 +++++++++++++ .../test_renderer_identity_from_config.py | 9 ++------- .../test_renderer_identity_from_environment.py | 9 ++------- 8 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 tests/integration/_test_renderer_identity.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4704eed..7166d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,5 @@ # saltstack-age change log + +## Unreleased + +* fix: add support for nested pillar data diff --git a/example/pillar/test.sls b/example/pillar/test.sls index 6279d1f..72b1543 100644 --- a/example/pillar/test.sls +++ b/example/pillar/test.sls @@ -1,7 +1,8 @@ #!jinja|yaml|age -prefix: /tmp +test: + prefix: /tmp -private: ENC[age-identity,YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkWHZYRkU2bjc4M2VtaElEZGxudmkwNW95ZHlNZy84K3U4MmlXejIzRkJNCktPbkhLU0h4VXBFYTZUUDlzbFFzdUx5R1VyaDZhd2doNkE2QnFpUmV6OFEKLS0tIFd3Wlg1UWQ3NHEwKyt6bTZkdmp3bWRCTTZkakppTFovbkhBcDhFeGdJazgKnf48DyGjBm2wOpM11YZ0+1btASDDSdgqXiM4SXXEMHhylmW8G9pSoTtovj0aZu9QVA==] + private: ENC[age-identity,YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkWHZYRkU2bjc4M2VtaElEZGxudmkwNW95ZHlNZy84K3U4MmlXejIzRkJNCktPbkhLU0h4VXBFYTZUUDlzbFFzdUx5R1VyaDZhd2doNkE2QnFpUmV6OFEKLS0tIFd3Wlg1UWQ3NHEwKyt6bTZkdmp3bWRCTTZkakppTFovbkhBcDhFeGdJazgKnf48DyGjBm2wOpM11YZ0+1btASDDSdgqXiM4SXXEMHhylmW8G9pSoTtovj0aZu9QVA==] -public: that's not a secret + public: that's not a secret diff --git a/example/states/test.sls b/example/states/test.sls index 39a1a7d..f4cfc3c 100644 --- a/example/states/test.sls +++ b/example/states/test.sls @@ -1,9 +1,9 @@ -{% set prefix = salt.pillar.get('prefix') %} +{% set prefix = salt.pillar.get('test:prefix') %} {{ prefix }}/test-public: file.managed: - - contents_pillar: public + - contents_pillar: test:public {{ prefix }}/test-private: file.managed: - - contents_pillar: private + - contents_pillar: test:private diff --git a/src/saltstack_age/renderers/age.py b/src/saltstack_age/renderers/age.py index 957372b..1ad166f 100644 --- a/src/saltstack_age/renderers/age.py +++ b/src/saltstack_age/renderers/age.py @@ -1,7 +1,7 @@ from collections import OrderedDict from importlib import import_module from pathlib import Path -from typing import Any +from typing import Any, cast import pyrage from salt.exceptions import SaltRenderError @@ -73,13 +73,18 @@ def _decrypt(string: str) -> str: return secure_value.decrypt(_get_passphrase()) +def _render_value(value: Any) -> Any: # noqa: ANN401 + if is_secure_value(value): + return _decrypt(value) + if isinstance(value, OrderedDict): + return render(cast(Data, value)) + return value + + def render( data: Data, _saltenv: str = "base", _sls: str = "", **_kwargs: None, ) -> Data: - return OrderedDict( - (key, _decrypt(value) if is_secure_value(value) else value) - for key, value in data.items() - ) + return OrderedDict((key, _render_value(value)) for key, value in data.items()) diff --git a/src/saltstack_age/secure_value.py b/src/saltstack_age/secure_value.py index 1580a37..f2f2b71 100644 --- a/src/saltstack_age/secure_value.py +++ b/src/saltstack_age/secure_value.py @@ -1,6 +1,7 @@ import re from base64 import b64decode from dataclasses import dataclass +from typing import Any import pyrage @@ -24,8 +25,8 @@ ) -def is_secure_value(string: str) -> bool: - return bool(RE_SECURE_VALUE.match(string)) +def is_secure_value(value: Any) -> bool: # noqa: ANN401 + return bool(RE_SECURE_VALUE.match(value)) if isinstance(value, str) else False @dataclass diff --git a/tests/integration/_test_renderer_identity.py b/tests/integration/_test_renderer_identity.py new file mode 100644 index 0000000..3407a04 --- /dev/null +++ b/tests/integration/_test_renderer_identity.py @@ -0,0 +1,13 @@ +import json +from pathlib import Path + +from saltfactories.cli.call import SaltCall + + +def test(salt_call_cli: SaltCall, tmp_path: Path) -> None: + _ = salt_call_cli.run( + "state.apply", + pillar=json.dumps({"test": {"prefix": str(tmp_path)}}), + ) + assert (tmp_path / "test-public").read_text() == "that's not a secret\n" + assert (tmp_path / "test-private").read_text() == "test-secret-value\n" diff --git a/tests/integration/test_renderer_identity_from_config.py b/tests/integration/test_renderer_identity_from_config.py index 976c994..84cefd3 100644 --- a/tests/integration/test_renderer_identity_from_config.py +++ b/tests/integration/test_renderer_identity_from_config.py @@ -1,11 +1,9 @@ -from pathlib import Path - import pytest -from saltfactories.cli.call import SaltCall from saltfactories.daemons.minion import SaltMinion from saltfactories.manager import FactoriesManager from saltfactories.utils import random_string +from tests.integration import _test_renderer_identity from tests.integration.conftest import MINION_CONFIG @@ -19,7 +17,4 @@ def minion(salt_factories: FactoriesManager, example_age_key: str) -> SaltMinion ) -def test(salt_call_cli: SaltCall, tmp_path: Path) -> None: - _ = salt_call_cli.run("state.apply", pillar=f'{{"prefix": "{tmp_path}"}}') - assert (tmp_path / "test-public").read_text() == "that's not a secret\n" - assert (tmp_path / "test-private").read_text() == "test-secret-value\n" +test = _test_renderer_identity.test diff --git a/tests/integration/test_renderer_identity_from_environment.py b/tests/integration/test_renderer_identity_from_environment.py index 3fdf9e4..b22dfbd 100644 --- a/tests/integration/test_renderer_identity_from_environment.py +++ b/tests/integration/test_renderer_identity_from_environment.py @@ -1,11 +1,9 @@ -from pathlib import Path - import pytest -from saltfactories.cli.call import SaltCall from saltfactories.daemons.minion import SaltMinion from saltfactories.manager import FactoriesManager from saltfactories.utils import random_string +from tests.integration import _test_renderer_identity from tests.integration.conftest import MINION_CONFIG @@ -22,7 +20,4 @@ def minion( ) -def test(salt_call_cli: SaltCall, tmp_path: Path) -> None: - _ = salt_call_cli.run("state.apply", pillar=f'{{"prefix": "{tmp_path}"}}') - assert (tmp_path / "test-public").read_text() == "that's not a secret\n" - assert (tmp_path / "test-private").read_text() == "test-secret-value\n" +test = _test_renderer_identity.test From d91f41f308202a153da7eb43e307bf3d8d4d8b05 Mon Sep 17 00:00:00 2001 From: Philippe Muller Date: Tue, 30 Apr 2024 21:42:22 +0800 Subject: [PATCH 2/5] fix(cli): write results to stdout --- CHANGELOG.md | 1 + src/saltstack_age/cli.py | 12 ++++++------ tests/integration/test_cli.py | 26 +++++++++----------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7166d77..ccf5800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,3 +3,4 @@ ## Unreleased * fix: add support for nested pillar data +* fix(cli): write results to stdout diff --git a/src/saltstack_age/cli.py b/src/saltstack_age/cli.py index 7ee685b..6865761 100644 --- a/src/saltstack_age/cli.py +++ b/src/saltstack_age/cli.py @@ -144,15 +144,15 @@ def determine_encryption_type( def encrypt(arguments: Namespace) -> None: value = get_value(arguments).encode() + type_ = determine_encryption_type(arguments) - if determine_encryption_type(arguments) == "identity": + if type_ == "identity": recipients = [identity.to_public() for identity in get_identities(arguments)] ciphertext = pyrage.encrypt(value, recipients) - LOGGER.info("ENC[age-identity,%s]", b64encode(ciphertext).decode()) - else: ciphertext = pyrage.passphrase.encrypt(value, get_passphrase(arguments)) - LOGGER.info("ENC[age-passphrase,%s]", b64encode(ciphertext).decode()) + + _ = sys.stdout.write(f"ENC[age-{type_},{b64encode(ciphertext).decode()}]\n") def decrypt(arguments: Namespace) -> None: @@ -172,10 +172,10 @@ def decrypt(arguments: Namespace) -> None: ) raise SystemExit(-1) - LOGGER.info("%s", secure_value.decrypt(arguments.identities[0])) + _ = sys.stdout.write(secure_value.decrypt(arguments.identities[0])) else: # isinstance(secure_value, PassphraseSecureValue) - LOGGER.info("%s", secure_value.decrypt(get_passphrase(arguments))) + _ = sys.stdout.write(secure_value.decrypt(get_passphrase(arguments))) def main(cli_args: Sequence[str] | None = None) -> None: diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 131bd21..bf8c0b1 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -1,4 +1,3 @@ -import logging from collections.abc import Sequence from pathlib import Path @@ -13,13 +12,11 @@ ) -def test_encrypt__passphrase(caplog: pytest.LogCaptureFixture) -> None: - # Only keep INFO log records - caplog.set_level(logging.INFO) +def test_encrypt__passphrase(capsys: pytest.CaptureFixture[str]) -> None: # Run the CLI tool main(["-P", "woah that is so secret", "enc", "another secret"]) # Ensure we get a passphrase secure value string - secure_value_string = caplog.record_tuples[0][2] + secure_value_string = capsys.readouterr().out secure_value = parse_secure_value(secure_value_string) assert isinstance(secure_value, PassphraseSecureValue) # Ensure we can decrypt it @@ -27,15 +24,13 @@ def test_encrypt__passphrase(caplog: pytest.LogCaptureFixture) -> None: def test_encrypt__single_recipient( - caplog: pytest.LogCaptureFixture, + capsys: pytest.CaptureFixture[str], example_age_key: str, ) -> None: - # Only keep INFO log records - caplog.set_level(logging.INFO) # Run the CLI tool main(["-i", example_age_key, "enc", "foo"]) # Ensure we get an identity secure value string - secure_value_string = caplog.record_tuples[0][2] + secure_value_string = capsys.readouterr().out secure_value = parse_secure_value(secure_value_string) assert isinstance(secure_value, IdentitySecureValue) # Ensure we can decrypt it using the same identity @@ -43,10 +38,9 @@ def test_encrypt__single_recipient( def test_encrypt__multiple_recipients( - caplog: pytest.LogCaptureFixture, tmp_path: Path + capsys: pytest.CaptureFixture[str], + tmp_path: Path, ) -> None: - # Only keep INFO log records - caplog.set_level(logging.INFO) # Generate identities identity1 = pyrage.x25519.Identity.generate() identity1_path = tmp_path / "identity1" @@ -66,7 +60,7 @@ def test_encrypt__multiple_recipients( ] ) # Ensure we get an identity secure value string - secure_value_string = caplog.record_tuples[0][2] + secure_value_string = capsys.readouterr().out secure_value = parse_secure_value(secure_value_string) assert isinstance(secure_value, IdentitySecureValue) # Ensure we can decrypt it using all the recipient identities @@ -114,15 +108,13 @@ def test_decrypt( environment: None | dict[str, str], args: Sequence[str], result: str, - caplog: pytest.LogCaptureFixture, + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, ) -> None: # Setup environment variables for name, value in (environment or {}).items(): monkeypatch.setenv(name, value) - # Only keep INFO log records - caplog.set_level(logging.INFO) # Run the CLI tool main(args) # Ensure we get the expected result - assert caplog.record_tuples == [("saltstack_age.cli", logging.INFO, result)] + assert capsys.readouterr().out == result From 0fba43379fefe72fdb75f4d67f5a43044dd9865f Mon Sep 17 00:00:00 2001 From: Philippe Muller Date: Tue, 30 Apr 2024 21:44:27 +0800 Subject: [PATCH 3/5] feat(ci): run tests --- .github/workflows/build.yaml | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9ff0e44..e5aed31 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,3 +15,4 @@ jobs: - run: rye fmt -- --check - run: rye check - run: rye run basedpyright + - run: rye run pytest -vvv diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf5800..9ac87f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,4 @@ * fix: add support for nested pillar data * fix(cli): write results to stdout +* feat(ci): run tests From 8dec45a55be2bf5830b65d2476189619de696eb2 Mon Sep 17 00:00:00 2001 From: Philippe Muller Date: Tue, 30 Apr 2024 21:48:51 +0800 Subject: [PATCH 4/5] feat: bump version to 0.3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8a16a07..0e38668 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "saltstack-age" -version = "0.2.3" +version = "0.3.0" description = "age renderer for Saltstack" authors = [{ name = "Philippe Muller" }] dependencies = [ From e99ac15dffdab7f55da568323950ed9c7d9f41e0 Mon Sep 17 00:00:00 2001 From: Philippe Muller Date: Tue, 30 Apr 2024 21:49:32 +0800 Subject: [PATCH 5/5] doc: update changelog for 0.3.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac87f1..2bf4d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # saltstack-age change log -## Unreleased +## 0.3.0 * fix: add support for nested pillar data * fix(cli): write results to stdout