Skip to content

Commit

Permalink
Merge pull request #24 from pmuller/develop
Browse files Browse the repository at this point in the history
Release 0.3.0
  • Loading branch information
pmuller authored Apr 30, 2024
2 parents e1cb58d + 9ca9965 commit 3734877
Show file tree
Hide file tree
Showing 12 changed files with 60 additions and 51 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ jobs:
- run: rye fmt -- --check
- run: rye check
- run: rye run basedpyright
- run: rye run pytest -vvv
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
# saltstack-age change log

## 0.3.0

* fix: add support for nested pillar data
* fix(cli): write results to stdout
* feat(ci): run tests
7 changes: 4 additions & 3 deletions example/pillar/test.sls
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions example/states/test.sls
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
12 changes: 6 additions & 6 deletions src/saltstack_age/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
15 changes: 10 additions & 5 deletions src/saltstack_age/renderers/age.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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())
5 changes: 3 additions & 2 deletions src/saltstack_age/secure_value.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
from base64 import b64decode
from dataclasses import dataclass
from typing import Any

import pyrage

Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions tests/integration/_test_renderer_identity.py
Original file line number Diff line number Diff line change
@@ -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"
26 changes: 9 additions & 17 deletions tests/integration/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import logging
from collections.abc import Sequence
from pathlib import Path

Expand All @@ -13,40 +12,35 @@
)


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
assert secure_value.decrypt("woah that is so secret") == "another secret"


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
assert secure_value.decrypt(read_identity_file(example_age_key)) == "foo"


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"
Expand All @@ -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
Expand Down Expand Up @@ -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
9 changes: 2 additions & 7 deletions tests/integration/test_renderer_identity_from_config.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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
9 changes: 2 additions & 7 deletions tests/integration/test_renderer_identity_from_environment.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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

0 comments on commit 3734877

Please sign in to comment.