updating groups/machines/users keys now also update vars secrets

This commit is contained in:
Jörg Thalheim
2024-12-17 19:21:45 +01:00
parent 5893a53089
commit f2856cb773
5 changed files with 113 additions and 34 deletions

View File

@@ -20,7 +20,6 @@ from .folders import (
sops_secrets_folder, sops_secrets_folder,
sops_users_folder, sops_users_folder,
) )
from .sops import update_keys
from .types import ( from .types import (
VALID_USER_NAME, VALID_USER_NAME,
group_name_type, group_name_type,
@@ -97,15 +96,10 @@ def list_directory(directory: Path) -> str:
def update_group_keys(flake_dir: Path, group: str) -> list[Path]: def update_group_keys(flake_dir: Path, group: str) -> list[Path]:
updated_paths = [] def filter_group_secrets(secret: Path) -> bool:
for secret_ in secrets.list_secrets(flake_dir): return (secret / "groups" / group).is_symlink()
secret = sops_secrets_folder(flake_dir) / secret_
if (secret / "groups" / group).is_symlink(): return secrets.update_secrets(flake_dir, filter_secrets=filter_group_secrets)
updated_paths += update_keys(
secret,
secrets.collect_keys_for_path(secret),
)
return updated_paths
def add_member( def add_member(
@@ -209,6 +203,21 @@ def add_secret(flake_dir: Path, group: str, name: str) -> None:
) )
def get_groups(
flake_dir: Path,
type_name: str,
name: str,
) -> list[Path]:
groups_dir = sops_groups_folder(flake_dir)
groups = []
if groups_dir.exists():
for group in groups_dir.iterdir():
if group.is_dir() and (group / type_name / name).exists():
groups.append(group)
return groups
def add_secret_command(args: argparse.Namespace) -> None: def add_secret_command(args: argparse.Namespace) -> None:
add_secret(args.flake.path, args.group, args.secret) add_secret(args.flake.path, args.group, args.secret)

View File

@@ -17,24 +17,29 @@ from .folders import (
sops_machines_folder, sops_machines_folder,
sops_secrets_folder, sops_secrets_folder,
) )
from .groups import get_groups
from .secrets import update_secrets from .secrets import update_secrets
from .sops import read_key, write_key from .sops import read_key, write_key
from .types import public_or_private_age_key_type, secret_name_type from .types import public_or_private_age_key_type, secret_name_type
def add_machine(flake_dir: Path, machine: str, pubkey: str, force: bool) -> None: def add_machine(flake_dir: Path, name: str, pubkey: str, force: bool) -> None:
machine_path = sops_machines_folder(flake_dir) / machine machine_path = sops_machines_folder(flake_dir) / name
write_key(machine_path, pubkey, sops.KeyType.AGE, overwrite=force) write_key(machine_path, pubkey, sops.KeyType.AGE, overwrite=force)
paths = [machine_path] paths = [machine_path]
groups = get_groups(flake_dir, "machines", name)
def filter_machine_secrets(secret: Path) -> bool: def filter_machine_secrets(secret: Path) -> bool:
return (secret / "machines" / machine).exists() if (secret / "machines" / name).exists():
return True
return any(secret.joinpath("groups", group.name).exists() for group in groups)
paths.extend(update_secrets(flake_dir, filter_secrets=filter_machine_secrets)) paths.extend(update_secrets(flake_dir, filter_secrets=filter_machine_secrets))
commit_files( commit_files(
paths, paths,
flake_dir, flake_dir,
f"Add machine {machine} to secrets", f"Add machine {name} to secrets",
) )

View File

@@ -1,4 +1,5 @@
import argparse import argparse
import functools
import getpass import getpass
import logging import logging
import os import os
@@ -39,18 +40,52 @@ from .types import VALID_SECRET_NAME, secret_name_type
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def list_generators_secrets(generators_path: Path) -> list[Path]:
for generator_path in generators_path.iterdir():
if not generator_path.is_dir():
continue
def validate(generator_path: Path, name: str) -> bool:
return has_secret(generator_path / name)
paths = []
for obj in list_objects(
generator_path, functools.partial(validate, generator_path)
):
paths.append(generator_path / obj)
return paths
return []
def list_vars_secrets(flake_dir: Path) -> list[Path]:
secret_paths = []
shared_dir = flake_dir / "vars" / "shared"
if shared_dir.is_dir():
secret_paths.extend(list_generators_secrets(shared_dir))
machines_dir = flake_dir / "vars" / "per-machine"
if machines_dir.is_dir():
for machine_dir in machines_dir.iterdir():
if not machine_dir.is_dir():
continue
secret_paths.extend(list_generators_secrets(machine_dir))
return secret_paths
def update_secrets( def update_secrets(
flake_dir: Path, filter_secrets: Callable[[Path], bool] = lambda _: True flake_dir: Path, filter_secrets: Callable[[Path], bool] = lambda _: True
) -> list[Path]: ) -> list[Path]:
changed_files = [] changed_files = []
for name in list_secrets(flake_dir): secret_paths = [sops_secrets_folder(flake_dir) / s for s in list_secrets(flake_dir)]
secret_path = sops_secrets_folder(flake_dir) / name secret_paths.extend(list_vars_secrets(flake_dir))
if not filter_secrets(secret_path):
for path in secret_paths:
if not filter_secrets(path):
continue continue
changed_files.extend( changed_files.extend(
update_keys( update_keys(
secret_path, path,
collect_keys_for_path(secret_path), collect_keys_for_path(path),
) )
) )
return changed_files return changed_files
@@ -222,7 +257,7 @@ def allow_member(
) -> list[Path]: ) -> list[Path]:
source = source_folder / name source = source_folder / name
if not source.exists(): if not source.exists():
msg = f"Cannot encrypt {group_folder.parent.name} for '{name}' group. '{name}' group does not exist in {source_folder}: " msg = f"Cannot encrypt {group_folder.parent.name} for '{name}'. '{name}' does not exist in {source_folder}: "
msg += list_directory(source_folder) msg += list_directory(source_folder)
raise ClanError(msg) raise ClanError(msg)
group_folder.mkdir(parents=True, exist_ok=True) group_folder.mkdir(parents=True, exist_ok=True)

View File

@@ -8,7 +8,13 @@ from clan_cli.errors import ClanError
from clan_cli.git import commit_files from clan_cli.git import commit_files
from . import secrets, sops from . import secrets, sops
from .folders import list_objects, remove_object, sops_secrets_folder, sops_users_folder from .folders import (
list_objects,
remove_object,
sops_secrets_folder,
sops_users_folder,
)
from .groups import get_groups
from .secrets import update_secrets from .secrets import update_secrets
from .sops import read_key, write_key from .sops import read_key, write_key
from .types import ( from .types import (
@@ -28,11 +34,16 @@ def add_user(
) -> None: ) -> None:
path = sops_users_folder(flake_dir) / name path = sops_users_folder(flake_dir) / name
groups = get_groups(flake_dir, "users", name)
def filter_user_secrets(secret: Path) -> bool: def filter_user_secrets(secret: Path) -> bool:
return secret.joinpath("users", name).exists() if secret.joinpath("users", name).exists():
return True
return any(secret.joinpath("groups", group.name).exists() for group in groups)
write_key(path, key, key_type, overwrite=force) write_key(path, key, key_type, overwrite=force)
paths = [path] paths = [path]
paths.extend(update_secrets(flake_dir, filter_secrets=filter_user_secrets)) paths.extend(update_secrets(flake_dir, filter_secrets=filter_user_secrets))
commit_files( commit_files(
paths, paths,

View File

@@ -2,6 +2,7 @@ import json
import logging import logging
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
import pytest import pytest
from age_keys import SopsSetup from age_keys import SopsSetup
@@ -20,6 +21,9 @@ from clan_cli.vars.set import set_var
from fixtures_flakes import ClanFlake from fixtures_flakes import ClanFlake
from helpers import cli from helpers import cli
if TYPE_CHECKING:
from age_keys import KeyPair
def test_dependencies_as_files(temp_dir: Path) -> None: def test_dependencies_as_files(temp_dir: Path) -> None:
from clan_cli.vars.generate import dependencies_as_dir from clan_cli.vars.generate import dependencies_as_dir
@@ -218,6 +222,7 @@ def test_generate_secret_var_sops_with_default_group(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake, flake: ClanFlake,
sops_setup: SopsSetup, sops_setup: SopsSetup,
age_keys: list["KeyPair"],
) -> None: ) -> None:
config = flake.machines["my_machine"] config = flake.machines["my_machine"]
config["nixpkgs"]["hostPlatform"] = "x86_64-linux" config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
@@ -242,10 +247,9 @@ def test_generate_secret_var_sops_with_default_group(
) )
assert sops_store.exists(Generator("my_generator"), "my_secret") assert sops_store.exists(Generator("my_generator"), "my_secret")
assert sops_store.get(Generator("my_generator"), "my_secret").decode() == "hello\n" assert sops_store.get(Generator("my_generator"), "my_secret").decode() == "hello\n"
# add another user and check if secret gets re-encrypted
from clan_cli.secrets.sops import generate_private_key
_, pubkey_uschi = generate_private_key() # add another user to the group and check if secret gets re-encrypted
pubkey_user2 = age_keys[1]
cli.run( cli.run(
[ [
"secrets", "secrets",
@@ -253,19 +257,34 @@ def test_generate_secret_var_sops_with_default_group(
"add", "add",
"--flake", "--flake",
str(flake.path), str(flake.path),
"uschi", "user2",
pubkey_uschi, pubkey_user2.pubkey,
] ]
) )
cli.run(["secrets", "groups", "add-user", "my_group", "uschi"]) cli.run(["secrets", "groups", "add-user", "my_group", "user2"])
with pytest.raises(ClanError):
cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"])
# apply fix
cli.run(["vars", "fix", "--flake", str(flake.path), "my_machine"])
# check if new user can access the secret # check if new user can access the secret
monkeypatch.setenv("USER", "uschi") monkeypatch.setenv("USER", "user2")
assert sops_store.user_has_access( assert sops_store.user_has_access(
"uschi", Generator("my_generator", share=False), "my_secret" "user2", Generator("my_generator", share=False), "my_secret"
)
# Rotate key of a user
pubkey_user3 = age_keys[2]
cli.run(
[
"secrets",
"users",
"add",
"--flake",
str(flake.path),
"--force",
"user2",
pubkey_user3.pubkey,
]
)
monkeypatch.setenv("USER", "user2")
assert sops_store.user_has_access(
"user2", Generator("my_generator", share=False), "my_secret"
) )