feat: configure age plugins for SOPS in buildClan

This commit is contained in:
Brian McGee
2025-04-28 12:54:27 +01:00
committed by Michael Hoang
parent d3e1c0b4e4
commit a438fe77a7
13 changed files with 357 additions and 121 deletions

View File

@@ -1,5 +1,11 @@
[
"age",
"age-plugin-fido2-hmac",
"age-plugin-ledger",
"age-plugin-se",
"age-plugin-sss",
"age-plugin-tpm",
"age-plugin-yubikey",
"avahi",
"bash",
"bubblewrap",

View File

@@ -240,6 +240,7 @@ def add_group_argument(parser: argparse.ArgumentParser) -> None:
def add_secret(flake_dir: Path, group: str, name: str) -> None:
secrets.allow_member(
flake_dir,
secrets.groups_folder(sops_secrets_folder(flake_dir) / name),
sops_groups_folder(flake_dir),
group,
@@ -267,7 +268,7 @@ def add_secret_command(args: argparse.Namespace) -> None:
def remove_secret(flake_dir: Path, group: str, name: str) -> None:
updated_paths = secrets.disallow_member(
secrets.groups_folder(sops_secrets_folder(flake_dir) / name), group
flake_dir, secrets.groups_folder(sops_secrets_folder(flake_dir) / name), group
)
commit_files(
updated_paths,

View File

@@ -74,6 +74,7 @@ def list_sops_machines(flake_dir: Path) -> list[str]:
def add_secret(flake_dir: Path, machine: str, secret_path: Path) -> None:
paths = secrets.allow_member(
flake_dir,
secrets.machines_folder(secret_path),
sops_machines_folder(flake_dir),
machine,
@@ -87,7 +88,9 @@ def add_secret(flake_dir: Path, machine: str, secret_path: Path) -> None:
def remove_secret(flake_dir: Path, machine: str, secret: str) -> None:
updated_paths = secrets.disallow_member(
secrets.machines_folder(sops_secrets_folder(flake_dir) / secret), machine
flake_dir,
secrets.machines_folder(sops_secrets_folder(flake_dir) / secret),
machine,
)
commit_files(
updated_paths,

View File

@@ -86,6 +86,7 @@ def update_secrets(
changed_files.extend(cleanup_dangling_symlinks(path / "machines"))
changed_files.extend(
update_keys(
flake_dir,
path,
collect_keys_for_path(path),
)
@@ -172,6 +173,7 @@ def encrypt_secret(
for user in add_users:
files_to_commit.extend(
allow_member(
flake_dir,
users_folder(secret_path),
sops_users_folder(flake_dir),
user,
@@ -182,6 +184,7 @@ def encrypt_secret(
for machine in add_machines:
files_to_commit.extend(
allow_member(
flake_dir,
machines_folder(secret_path),
sops_machines_folder(flake_dir),
machine,
@@ -192,6 +195,7 @@ def encrypt_secret(
for group in add_groups:
files_to_commit.extend(
allow_member(
flake_dir,
groups_folder(secret_path),
sops_groups_folder(flake_dir),
group,
@@ -206,6 +210,7 @@ def encrypt_secret(
files_to_commit.extend(
allow_member(
flake_dir,
users_folder(secret_path),
sops_users_folder(flake_dir),
username,
@@ -214,7 +219,7 @@ def encrypt_secret(
)
secret_path = secret_path / "secret"
encrypt_file(secret_path, value, sorted(recipient_keys))
encrypt_file(flake_dir, secret_path, value, sorted(recipient_keys))
files_to_commit.append(secret_path)
if git_commit:
commit_files(
@@ -274,7 +279,11 @@ def list_directory(directory: Path) -> str:
def allow_member(
group_folder: Path, source_folder: Path, name: str, do_update_keys: bool = True
flake_dir: str | Path,
group_folder: Path,
source_folder: Path,
name: str,
do_update_keys: bool = True,
) -> list[Path]:
source = source_folder / name
if not source.exists():
@@ -297,6 +306,7 @@ def allow_member(
if do_update_keys:
changed.extend(
update_keys(
flake_dir,
group_folder.parent,
collect_keys_for_path(group_folder.parent),
)
@@ -304,7 +314,7 @@ def allow_member(
return changed
def disallow_member(group_folder: Path, name: str) -> list[Path]:
def disallow_member(flake_dir: str | Path, group_folder: Path, name: str) -> list[Path]:
target = group_folder / name
if not target.exists():
msg = f"{name} does not exist in group in {group_folder}: "
@@ -324,7 +334,9 @@ def disallow_member(group_folder: Path, name: str) -> list[Path]:
if next(group_folder.parent.iterdir(), None) is None:
group_folder.parent.rmdir()
return update_keys(target.parent.parent, collect_keys_for_path(group_folder.parent))
return update_keys(
flake_dir, target.parent.parent, collect_keys_for_path(group_folder.parent)
)
def has_secret(secret_path: Path) -> bool:
@@ -364,7 +376,7 @@ def decrypt_secret(flake_dir: Path, secret_path: Path) -> str:
if not path.exists():
msg = f"Secret '{secret_path!s}' does not exist"
raise ClanError(msg)
return decrypt_file(path)
return decrypt_file(flake_dir, path)
def get_command(args: argparse.Namespace) -> None:

View File

@@ -18,7 +18,7 @@ from clan_lib.api import API
from clan_cli.cmd import Log, RunOpts, run
from clan_cli.dirs import user_config_dir
from clan_cli.errors import ClanError
from clan_cli.nix import nix_shell
from clan_cli.nix import nix_eval, nix_shell
from .folders import sops_users_folder
@@ -191,7 +191,41 @@ class Operation(enum.StrEnum):
UPDATE_KEYS = "updatekeys"
def load_age_plugins(flake_dir: str | Path) -> list[str]:
if not flake_dir:
msg = "Missing flake directory"
raise ClanError(msg)
cmd = nix_eval(
[
f"{flake_dir}#clanInternals.secrets.age.plugins",
"--json",
]
)
try:
result = run(cmd)
except Exception as e:
msg = f"Failed to load age plugins {flake_dir}"
raise ClanError(msg) from e
json_str = result.stdout.strip()
try:
plugins = json.loads(json_str)
except json.JSONDecodeError as e:
msg = f"Failed to decode '{json_str}': {e}"
raise ClanError(msg) from e
if isinstance(plugins, list):
return plugins
msg = f"Expected a list of age plugins but {type(plugins)!r} was provided"
raise ClanError(msg)
def sops_run(
flake_dir: str | Path,
call: Operation,
secret_path: Path,
public_keys: Iterable[SopsKey],
@@ -249,7 +283,9 @@ def sops_run(
raise ClanError(msg)
sops_cmd.append(str(secret_path))
cmd = nix_shell(["sops", "gnupg"], sops_cmd)
age_plugins = load_age_plugins(flake_dir)
cmd = nix_shell(["sops", "gnupg", *age_plugins], sops_cmd)
opts = (
dataclasses.replace(run_opts, env=environ)
if run_opts
@@ -411,11 +447,14 @@ def ensure_admin_public_keys(flake_dir: Path) -> set[SopsKey]:
return user_keys
def update_keys(secret_path: Path, keys: Iterable[SopsKey]) -> list[Path]:
def update_keys(
flake_dir: str | Path, secret_path: Path, keys: Iterable[SopsKey]
) -> list[Path]:
secret_path = secret_path / "secret"
error_msg = f"Could not update keys for {secret_path}"
rc, _ = sops_run(
flake_dir,
Operation.UPDATE_KEYS,
secret_path,
keys,
@@ -426,6 +465,7 @@ def update_keys(secret_path: Path, keys: Iterable[SopsKey]) -> list[Path]:
def encrypt_file(
flake_dir: str | Path,
secret_path: Path,
content: str | IO[bytes] | bytes | None,
pubkeys: list[SopsKey],
@@ -436,6 +476,7 @@ def encrypt_file(
if not content:
# This will spawn an editor to edit the file.
rc, _ = sops_run(
flake_dir,
Operation.EDIT,
secret_path,
pubkeys,
@@ -474,6 +515,7 @@ def encrypt_file(
msg = f"Invalid content type: {type(content)}"
raise ClanError(msg)
sops_run(
flake_dir,
Operation.ENCRYPT,
Path(source.name),
pubkeys,
@@ -488,11 +530,12 @@ def encrypt_file(
Path(source.name).unlink()
def decrypt_file(secret_path: Path) -> str:
def decrypt_file(flake_dir: str | Path, secret_path: Path) -> str:
# decryption uses private keys from the environment or default paths:
no_public_keys_needed: list[SopsKey] = []
_, stdout = sops_run(
flake_dir,
Operation.DECRYPT,
secret_path,
no_public_keys_needed,

View File

@@ -93,6 +93,7 @@ def list_users(flake_dir: Path) -> list[str]:
def add_secret(flake_dir: Path, user: str, secret: str) -> None:
updated_paths = secrets.allow_member(
flake_dir,
secrets.users_folder(sops_secrets_folder(flake_dir) / secret),
sops_users_folder(flake_dir),
user,
@@ -106,7 +107,7 @@ def add_secret(flake_dir: Path, user: str, secret: str) -> None:
def remove_secret(flake_dir: Path, user: str, secret: str) -> None:
updated_paths = secrets.disallow_member(
secrets.users_folder(sops_secrets_folder(flake_dir) / secret), user
flake_dir, secrets.users_folder(sops_secrets_folder(flake_dir) / secret), user
)
commit_files(
updated_paths,

View File

@@ -10,9 +10,10 @@ if TYPE_CHECKING:
from .age_keys import KeyPair
@pytest.mark.with_core
def test_import_sops(
test_root: Path,
test_flake: FlakeForTest,
test_flake_with_core: FlakeForTest,
capture_output: CaptureOutput,
monkeypatch: pytest.MonkeyPatch,
age_keys: list["KeyPair"],
@@ -24,7 +25,7 @@ def test_import_sops(
"machines",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"machine1",
age_keys[0].pubkey,
]
@@ -35,7 +36,7 @@ def test_import_sops(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"user1",
age_keys[1].pubkey,
]
@@ -46,7 +47,7 @@ def test_import_sops(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"user2",
age_keys[2].pubkey,
]
@@ -57,7 +58,7 @@ def test_import_sops(
"groups",
"add-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"user1",
]
@@ -68,7 +69,7 @@ def test_import_sops(
"groups",
"add-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"user2",
]
@@ -80,7 +81,7 @@ def test_import_sops(
"secrets",
"import-sops",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"--group",
"group1",
"--machine",
@@ -90,11 +91,13 @@ def test_import_sops(
cli.run(cmd)
with capture_output as output:
cli.run(["secrets", "users", "list", "--flake", str(test_flake.path)])
cli.run(["secrets", "users", "list", "--flake", str(test_flake_with_core.path)])
users = sorted(output.out.rstrip().split())
assert users == ["user1", "user2"]
with capture_output as output:
cli.run(["secrets", "get", "--flake", str(test_flake.path), "secret-key"])
cli.run(
["secrets", "get", "--flake", str(test_flake_with_core.path), "secret-key"]
)
assert output.out == "secret-value"

View File

@@ -22,14 +22,15 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
@pytest.mark.with_core
def _test_identities(
what: str,
test_flake: FlakeForTest,
test_flake_with_core: FlakeForTest,
capture_output: CaptureOutput,
age_keys: list["KeyPair"],
monkeypatch: pytest.MonkeyPatch,
) -> None:
sops_folder = test_flake.path / "sops"
sops_folder = test_flake_with_core.path / "sops"
what_singular = what[:-1]
test_secret_name = f"{what_singular}_secret"
@@ -45,7 +46,7 @@ def _test_identities(
what,
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"foo",
age_keys[0].pubkey,
]
@@ -58,7 +59,7 @@ def _test_identities(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin",
admin_age_key.pubkey,
]
@@ -71,7 +72,7 @@ def _test_identities(
what,
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"foo",
age_keys[0].pubkey,
]
@@ -86,7 +87,7 @@ def _test_identities(
"secrets",
"set",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
f"--{what_singular}",
"foo",
test_secret_name,
@@ -94,7 +95,7 @@ def _test_identities(
)
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
test_secret_name,
expected_age_recipients_keypairs=[age_keys[0], admin_age_key],
)
@@ -107,14 +108,14 @@ def _test_identities(
what,
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"-f",
"foo",
age_keys[1].privkey,
]
)
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
test_secret_name,
expected_age_recipients_keypairs=[age_keys[1], admin_age_key],
)
@@ -126,24 +127,35 @@ def _test_identities(
what,
"get",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"foo",
]
)
assert age_keys[1].pubkey in output.out
with capture_output as output:
cli.run(["secrets", what, "list", "--flake", str(test_flake.path)])
cli.run(["secrets", what, "list", "--flake", str(test_flake_with_core.path)])
assert "foo" in output.out
cli.run(["secrets", what, "remove", "--flake", str(test_flake.path), "foo"])
cli.run(
["secrets", what, "remove", "--flake", str(test_flake_with_core.path), "foo"]
)
assert not (sops_folder / what / "foo" / "key.json").exists()
with pytest.raises(ClanError): # already removed
cli.run(["secrets", what, "remove", "--flake", str(test_flake.path), "foo"])
cli.run(
[
"secrets",
what,
"remove",
"--flake",
str(test_flake_with_core.path),
"foo",
]
)
with capture_output as output:
cli.run(["secrets", what, "list", "--flake", str(test_flake.path)])
cli.run(["secrets", what, "list", "--flake", str(test_flake_with_core.path)])
assert "foo" not in output.out
user_or_machine_symlink = sops_folder / "secrets" / test_secret_name / what / "foo"
@@ -155,23 +167,27 @@ def _test_identities(
assert not user_or_machine_symlink.exists(follow_symlinks=False), err_msg
@pytest.mark.with_core
def test_users(
test_flake: FlakeForTest,
test_flake_with_core: FlakeForTest,
capture_output: CaptureOutput,
age_keys: list["KeyPair"],
monkeypatch: pytest.MonkeyPatch,
) -> None:
with monkeypatch.context():
_test_identities("users", test_flake, capture_output, age_keys, monkeypatch)
_test_identities(
"users", test_flake_with_core, capture_output, age_keys, monkeypatch
)
@pytest.mark.with_core
def test_multiple_user_keys(
test_flake: FlakeForTest,
test_flake_with_core: FlakeForTest,
capture_output: CaptureOutput,
age_keys: list["KeyPair"],
monkeypatch: pytest.MonkeyPatch,
) -> None:
sops_folder = test_flake.path / "sops"
sops_folder = test_flake_with_core.path / "sops"
users_keys = {
"bob": {age_keys[0], age_keys[1]},
@@ -187,7 +203,7 @@ def test_multiple_user_keys(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
user,
*[f"--age-key={key.pubkey}" for key in user_keys],
]
@@ -196,7 +212,16 @@ def test_multiple_user_keys(
# check they are returned in get
with capture_output as output:
cli.run(["secrets", "users", "get", "--flake", str(test_flake.path), user])
cli.run(
[
"secrets",
"users",
"get",
"--flake",
str(test_flake_with_core.path),
user,
]
)
for user_key in user_keys:
assert user_key.pubkey in output.out
@@ -220,14 +245,14 @@ def test_multiple_user_keys(
"secrets",
"set",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
secret_name,
]
)
# check the secret has each of our user's keys as a recipient
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
secret_name,
expected_age_recipients_keypairs=[*user_keys],
)
@@ -235,7 +260,13 @@ def test_multiple_user_keys(
# check we can get the secret
with capture_output as output:
cli.run(
["secrets", "get", "--flake", str(test_flake.path), secret_name]
[
"secrets",
"get",
"--flake",
str(test_flake_with_core.path),
secret_name,
]
)
assert secret_value in output.out
@@ -259,7 +290,7 @@ def test_multiple_user_keys(
"users",
"remove-key",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
user,
key_to_remove.pubkey,
]
@@ -267,7 +298,7 @@ def test_multiple_user_keys(
# check the secret has been updated
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
secret_name,
expected_age_recipients_keypairs=list({*user_keys} - {key_to_remove}),
)
@@ -279,7 +310,7 @@ def test_multiple_user_keys(
"users",
"add-key",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
user,
key_to_remove.pubkey,
]
@@ -287,29 +318,35 @@ def test_multiple_user_keys(
# check the secret has been updated
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
secret_name,
expected_age_recipients_keypairs=user_keys,
)
@pytest.mark.with_core
def test_machines(
test_flake: FlakeForTest,
test_flake_with_core: FlakeForTest,
capture_output: CaptureOutput,
age_keys: list["KeyPair"],
monkeypatch: pytest.MonkeyPatch,
) -> None:
_test_identities("machines", test_flake, capture_output, age_keys, monkeypatch)
_test_identities(
"machines", test_flake_with_core, capture_output, age_keys, monkeypatch
)
@pytest.mark.with_core
def test_groups(
test_flake: FlakeForTest,
test_flake_with_core: FlakeForTest,
capture_output: CaptureOutput,
age_keys: list["KeyPair"],
monkeypatch: pytest.MonkeyPatch,
) -> None:
with capture_output as output:
cli.run(["secrets", "groups", "list", "--flake", str(test_flake.path)])
cli.run(
["secrets", "groups", "list", "--flake", str(test_flake_with_core.path)]
)
assert output.out == ""
machine1_age_key = age_keys[0]
@@ -323,7 +360,7 @@ def test_groups(
"groups",
"add-machine",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"machine1",
]
@@ -335,7 +372,7 @@ def test_groups(
"groups",
"add-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"groupb1",
"user1",
]
@@ -346,7 +383,7 @@ def test_groups(
"machines",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"machine1",
machine1_age_key.pubkey,
]
@@ -357,7 +394,7 @@ def test_groups(
"groups",
"add-machine",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"machine1",
]
@@ -370,7 +407,7 @@ def test_groups(
"groups",
"add-machine",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"machine1",
]
@@ -382,7 +419,7 @@ def test_groups(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"user1",
user1_age_key.pubkey,
]
@@ -393,7 +430,7 @@ def test_groups(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin",
admin_age_key.pubkey,
]
@@ -404,14 +441,16 @@ def test_groups(
"groups",
"add-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"user1",
]
)
with capture_output as output:
cli.run(["secrets", "groups", "list", "--flake", str(test_flake.path)])
cli.run(
["secrets", "groups", "list", "--flake", str(test_flake_with_core.path)]
)
out = output.out
assert "user1" in out
assert "machine1" in out
@@ -427,7 +466,7 @@ def test_groups(
"secrets",
"set",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"--group",
"group1",
secret_name,
@@ -435,7 +474,7 @@ def test_groups(
)
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
secret_name,
expected_age_recipients_keypairs=[
machine1_age_key,
@@ -454,13 +493,13 @@ def test_groups(
"groups",
"remove-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"user1",
]
)
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
secret_name,
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
err_msg=(
@@ -476,13 +515,13 @@ def test_groups(
"groups",
"add-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"user1",
]
)
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
secret_name,
expected_age_recipients_keypairs=[
machine1_age_key,
@@ -498,12 +537,12 @@ def test_groups(
"users",
"remove",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"user1",
]
)
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
secret_name,
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
err_msg=(
@@ -518,13 +557,13 @@ def test_groups(
"groups",
"remove-machine",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"group1",
"machine1",
]
)
assert_secrets_file_recipients(
test_flake.path,
test_flake_with_core.path,
secret_name,
expected_age_recipients_keypairs=[admin_age_key],
err_msg=(
@@ -533,11 +572,11 @@ def test_groups(
),
)
first_group = next((test_flake.path / "sops" / "groups").iterdir(), None)
first_group = next((test_flake_with_core.path / "sops" / "groups").iterdir(), None)
assert first_group is None
# Check if the symlink to the group was removed from our foo test secret:
group_symlink = test_flake.path / "sops/secrets/foo/groups/group1"
group_symlink = test_flake_with_core.path / "sops/secrets/foo/groups/group1"
err_msg = (
"Symlink to group1's key in foo secret "
"was not cleaned up after group1 was removed"
@@ -574,25 +613,30 @@ def use_gpg_key(key: GpgKey, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.setenv("SOPS_AGE_KEY", old_key)
@pytest.mark.with_core
def test_secrets(
test_flake: FlakeForTest,
test_flake_with_core: FlakeForTest,
capture_output: CaptureOutput,
monkeypatch: pytest.MonkeyPatch,
gpg_key: GpgKey,
age_keys: list["KeyPair"],
) -> None:
with capture_output as output:
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
cli.run(["secrets", "list", "--flake", str(test_flake_with_core.path)])
assert output.out == ""
# Generate a new key for the clan
monkeypatch.setenv("SOPS_AGE_KEY_FILE", str(test_flake.path / ".." / "age.key"))
monkeypatch.setenv(
"SOPS_AGE_KEY_FILE", str(test_flake_with_core.path / ".." / "age.key")
)
with capture_output as output:
cli.run(["secrets", "key", "generate", "--flake", str(test_flake.path)])
cli.run(
["secrets", "key", "generate", "--flake", str(test_flake_with_core.path)]
)
assert "age private key" in output.out
# Read the key that was generated
with capture_output as output:
cli.run(["secrets", "key", "show", "--flake", str(test_flake.path)])
cli.run(["secrets", "key", "show", "--flake", str(test_flake_with_core.path)])
key = json.loads(output.out)
assert key["publickey"].startswith("age1")
@@ -603,41 +647,71 @@ def test_secrets(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"testuser",
key["publickey"],
]
)
with pytest.raises(ClanError): # does not exist yet
cli.run(["secrets", "get", "--flake", str(test_flake.path), "nonexisting"])
cli.run(
["secrets", "get", "--flake", str(test_flake_with_core.path), "nonexisting"]
)
monkeypatch.setenv("SOPS_NIX_SECRET", "foo")
cli.run(["secrets", "set", "--flake", str(test_flake.path), "initialkey"])
cli.run(["secrets", "set", "--flake", str(test_flake_with_core.path), "initialkey"])
with capture_output as output:
cli.run(["secrets", "get", "--flake", str(test_flake.path), "initialkey"])
cli.run(
["secrets", "get", "--flake", str(test_flake_with_core.path), "initialkey"]
)
assert output.out == "foo"
with capture_output as output:
cli.run(["secrets", "users", "list", "--flake", str(test_flake.path)])
cli.run(["secrets", "users", "list", "--flake", str(test_flake_with_core.path)])
users = output.out.rstrip().split("\n")
assert len(users) == 1, f"users: {users}"
owner = users[0]
monkeypatch.setenv("EDITOR", "cat")
cli.run(["secrets", "set", "--edit", "--flake", str(test_flake.path), "initialkey"])
cli.run(
[
"secrets",
"set",
"--edit",
"--flake",
str(test_flake_with_core.path),
"initialkey",
]
)
monkeypatch.delenv("EDITOR")
cli.run(["secrets", "rename", "--flake", str(test_flake.path), "initialkey", "key"])
cli.run(
[
"secrets",
"rename",
"--flake",
str(test_flake_with_core.path),
"initialkey",
"key",
]
)
with capture_output as output:
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
cli.run(["secrets", "list", "--flake", str(test_flake_with_core.path)])
assert output.out == "key\n"
with capture_output as output:
cli.run(["secrets", "list", "--flake", str(test_flake.path), "nonexisting"])
cli.run(
[
"secrets",
"list",
"--flake",
str(test_flake_with_core.path),
"nonexisting",
]
)
assert output.out == ""
with capture_output as output:
cli.run(["secrets", "list", "--flake", str(test_flake.path), "key"])
cli.run(["secrets", "list", "--flake", str(test_flake_with_core.path), "key"])
assert output.out == "key\n"
# using the `age_keys` KeyPair, add a machine and rotate its key
@@ -648,7 +722,7 @@ def test_secrets(
"machines",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"machine1",
age_keys[1].pubkey,
]
@@ -659,18 +733,22 @@ def test_secrets(
"machines",
"add-secret",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"machine1",
"key",
]
)
with capture_output as output:
cli.run(["secrets", "machines", "list", "--flake", str(test_flake.path)])
cli.run(
["secrets", "machines", "list", "--flake", str(test_flake_with_core.path)]
)
assert output.out == "machine1\n"
with use_age_key(age_keys[1].privkey, monkeypatch):
with capture_output as output:
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
cli.run(
["secrets", "get", "--flake", str(test_flake_with_core.path), "key"]
)
assert output.out == "foo"
# rotate machines key
@@ -680,7 +758,7 @@ def test_secrets(
"machines",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"-f",
"machine1",
age_keys[0].privkey,
@@ -690,7 +768,9 @@ def test_secrets(
# should also rotate the encrypted secret
with use_age_key(age_keys[0].privkey, monkeypatch):
with capture_output as output:
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
cli.run(
["secrets", "get", "--flake", str(test_flake_with_core.path), "key"]
)
assert output.out == "foo"
cli.run(
@@ -699,7 +779,7 @@ def test_secrets(
"machines",
"remove-secret",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"machine1",
"key",
]
@@ -711,7 +791,7 @@ def test_secrets(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"user1",
age_keys[1].pubkey,
]
@@ -722,13 +802,13 @@ def test_secrets(
"users",
"add-secret",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"user1",
"key",
]
)
with capture_output as output, use_age_key(age_keys[1].privkey, monkeypatch):
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
cli.run(["secrets", "get", "--flake", str(test_flake_with_core.path), "key"])
assert output.out == "foo"
cli.run(
[
@@ -736,7 +816,7 @@ def test_secrets(
"users",
"remove-secret",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"user1",
"key",
]
@@ -749,7 +829,7 @@ def test_secrets(
"groups",
"add-secret",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin-group",
"key",
]
@@ -760,7 +840,7 @@ def test_secrets(
"groups",
"add-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin-group",
"user1",
]
@@ -771,7 +851,7 @@ def test_secrets(
"groups",
"add-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin-group",
owner,
]
@@ -782,7 +862,7 @@ def test_secrets(
"groups",
"add-secret",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin-group",
"key",
]
@@ -793,7 +873,7 @@ def test_secrets(
"secrets",
"set",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"--group",
"admin-group",
"key2",
@@ -802,7 +882,9 @@ def test_secrets(
with use_age_key(age_keys[1].privkey, monkeypatch):
with capture_output as output:
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
cli.run(
["secrets", "get", "--flake", str(test_flake_with_core.path), "key"]
)
assert output.out == "foo"
# Add an user with a GPG key
@@ -812,7 +894,7 @@ def test_secrets(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"--pgp-key",
gpg_key.fingerprint,
"user2",
@@ -826,7 +908,7 @@ def test_secrets(
"groups",
"add-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin-group",
"user2",
]
@@ -834,7 +916,9 @@ def test_secrets(
with use_gpg_key(gpg_key, monkeypatch): # user2
with capture_output as output:
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
cli.run(
["secrets", "get", "--flake", str(test_flake_with_core.path), "key"]
)
assert output.out == "foo"
cli.run(
@@ -843,7 +927,7 @@ def test_secrets(
"groups",
"remove-user",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin-group",
"user2",
]
@@ -854,7 +938,7 @@ def test_secrets(
capture_output as output,
):
# user2 is not in the group anymore
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
cli.run(["secrets", "get", "--flake", str(test_flake_with_core.path), "key"])
print(output.out)
cli.run(
@@ -863,22 +947,23 @@ def test_secrets(
"groups",
"remove-secret",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"admin-group",
"key",
]
)
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key"])
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key2"])
cli.run(["secrets", "remove", "--flake", str(test_flake_with_core.path), "key"])
cli.run(["secrets", "remove", "--flake", str(test_flake_with_core.path), "key2"])
with capture_output as output:
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
cli.run(["secrets", "list", "--flake", str(test_flake_with_core.path)])
assert output.out == ""
@pytest.mark.with_core
def test_secrets_key_generate_gpg(
test_flake: FlakeForTest,
test_flake_with_core: FlakeForTest,
capture_output: CaptureOutput,
monkeypatch: pytest.MonkeyPatch,
gpg_key: GpgKey,
@@ -893,14 +978,16 @@ def test_secrets_key_generate_gpg(
"key",
"generate",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
]
)
assert "age private key" not in output.out
assert re.match(r"PGP key.+is already set", output.err) is not None
with capture_output as output:
cli.run(["secrets", "key", "show", "--flake", str(test_flake.path)])
cli.run(
["secrets", "key", "show", "--flake", str(test_flake_with_core.path)]
)
key = json.loads(output.out)
assert key["type"] == "pgp"
assert key["publickey"] == gpg_key.fingerprint
@@ -912,7 +999,7 @@ def test_secrets_key_generate_gpg(
"users",
"add",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"--pgp-key",
gpg_key.fingerprint,
"testuser",
@@ -926,7 +1013,7 @@ def test_secrets_key_generate_gpg(
"users",
"get",
"--flake",
str(test_flake.path),
str(test_flake_with_core.path),
"testuser",
]
)
@@ -940,9 +1027,23 @@ def test_secrets_key_generate_gpg(
with monkeypatch.context() as m:
m.setenv("SOPS_NIX_SECRET", "secret-value")
cli.run(["secrets", "set", "--flake", str(test_flake.path), "secret-name"])
cli.run(
[
"secrets",
"set",
"--flake",
str(test_flake_with_core.path),
"secret-name",
]
)
with capture_output as output:
cli.run(
["secrets", "get", "--flake", str(test_flake.path), "secret-name"]
[
"secrets",
"get",
"--flake",
str(test_flake_with_core.path),
"secret-name",
]
)
assert output.out == "secret-value"

View File

@@ -302,6 +302,7 @@ class SecretStore(StoreBase):
for group in self.machine.deployment["sops"]["defaultGroups"]:
allow_member(
self.machine.flake_dir,
groups_folder(secret_path),
sops_groups_folder(self.machine.flake_dir),
group,
@@ -310,6 +311,7 @@ class SecretStore(StoreBase):
)
update_keys(
self.machine.flake_dir,
secret_path,
collect_keys_for_path(secret_path),
)