vars/sops: improve shared secrets, switching backend
When a second machine checks for a shared secret, now the exists() call returns negative and only when updating the secrets for that machine, the machine is added to the sops receivers. Also throw proper errors when the user switches backends without cleaning the files first.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import logging
|
||||
import shutil
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.machines import machines
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -113,6 +113,13 @@ class StoreBase(ABC):
|
||||
def is_secret_store(self) -> bool:
|
||||
pass
|
||||
|
||||
def backend_collision_error(self, folder: Path) -> None:
|
||||
msg = (
|
||||
f"Var folder {folder} exists but doesn't look like a {self.store_name} secret."
|
||||
"Potentially a leftover from another backend. Please delete it manually."
|
||||
)
|
||||
raise ClanError(msg)
|
||||
|
||||
def rel_dir(self, generator_name: str, var_name: str, shared: bool = False) -> Path:
|
||||
if shared:
|
||||
return Path(f"shared/{generator_name}/{var_name}")
|
||||
@@ -145,12 +152,6 @@ class StoreBase(ABC):
|
||||
else:
|
||||
old_val = None
|
||||
old_val_str = "<not set>"
|
||||
directory = self.directory(generator_name, var_name, shared)
|
||||
# delete directory
|
||||
if directory.exists():
|
||||
shutil.rmtree(directory)
|
||||
# re-create directory
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
new_file = self._set(generator_name, var_name, value, shared, deployed)
|
||||
if self.is_secret_store:
|
||||
print(f"Updated secret var {generator_name}/{var_name}\n")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.errors import ClanError
|
||||
@@ -23,14 +24,22 @@ class FactStore(FactStoreBase):
|
||||
shared: bool = False,
|
||||
deployed: bool = True,
|
||||
) -> Path | None:
|
||||
if self.machine.flake.is_local():
|
||||
file_path = self.directory(generator_name, name, shared) / "value"
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.touch()
|
||||
file_path.write_bytes(value)
|
||||
return file_path
|
||||
msg = f"in_flake fact storage is only supported for local flakes: {self.machine.flake}"
|
||||
raise ClanError(msg)
|
||||
if not self.machine.flake.is_local():
|
||||
msg = f"in_flake fact storage is only supported for local flakes: {self.machine.flake}"
|
||||
raise ClanError(msg)
|
||||
folder = self.directory(generator_name, name, shared)
|
||||
if folder.exists():
|
||||
if not (folder / "value").exists():
|
||||
# another backend has used that folder before -> error out
|
||||
self.backend_collision_error(folder)
|
||||
shutil.rmtree(folder)
|
||||
# re-create directory
|
||||
file_path = folder / "value"
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.touch()
|
||||
file_path.write_bytes(value)
|
||||
return file_path
|
||||
|
||||
# get a single fact
|
||||
def get(self, generator_name: str, name: str, shared: bool = False) -> bytes:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.machines.machines import Machine
|
||||
from clan_cli.secrets.folders import sops_secrets_folder
|
||||
from clan_cli.secrets.folders import sops_machines_folder, sops_secrets_folder
|
||||
from clan_cli.secrets.machines import add_machine, add_secret, has_machine
|
||||
from clan_cli.secrets.secrets import decrypt_secret, encrypt_secret, has_secret
|
||||
from clan_cli.secrets.sops import generate_private_key
|
||||
@@ -40,6 +41,18 @@ class SecretStore(SecretStoreBase):
|
||||
def store_name(self) -> str:
|
||||
return "sops"
|
||||
|
||||
def machine_has_access(
|
||||
self, generator_name: str, secret_name: str, shared: bool
|
||||
) -> bool:
|
||||
secret_path = self.secret_path(generator_name, secret_name, shared)
|
||||
secret = json.loads((secret_path / "secret").read_text())
|
||||
recipients = [r["recipient"] for r in secret["sops"]["age"]]
|
||||
machines_folder_path = sops_machines_folder(self.machine.flake_dir)
|
||||
machine_pubkey = json.loads(
|
||||
(machines_folder_path / self.machine.name / "key.json").read_text()
|
||||
)["publickey"]
|
||||
return machine_pubkey in recipients
|
||||
|
||||
def secret_path(
|
||||
self, generator_name: str, secret_name: str, shared: bool = False
|
||||
) -> Path:
|
||||
@@ -53,16 +66,28 @@ class SecretStore(SecretStoreBase):
|
||||
shared: bool = False,
|
||||
deployed: bool = True,
|
||||
) -> Path | None:
|
||||
path = self.secret_path(generator_name, name, shared)
|
||||
encrypt_secret(
|
||||
self.machine.flake_dir,
|
||||
path,
|
||||
value,
|
||||
add_machines=[self.machine.name],
|
||||
add_groups=self.machine.deployment["sops"]["defaultGroups"],
|
||||
git_commit=False,
|
||||
)
|
||||
return path
|
||||
secret_folder = self.secret_path(generator_name, name, shared)
|
||||
# delete directory
|
||||
if secret_folder.exists() and not (secret_folder / "secret").exists():
|
||||
# another backend has used that folder before -> error out
|
||||
self.backend_collision_error(secret_folder)
|
||||
# create directory if it doesn't exist
|
||||
secret_folder.mkdir(parents=True, exist_ok=True)
|
||||
if shared and self.exists_shared(generator_name, name):
|
||||
# secret exists, but this machine doesn't have access -> add machine
|
||||
# add_secret will be a no-op if the machine is already added
|
||||
add_secret(self.machine.flake_dir, self.machine.name, secret_folder)
|
||||
else:
|
||||
# initialize the secret
|
||||
encrypt_secret(
|
||||
self.machine.flake_dir,
|
||||
secret_folder,
|
||||
value,
|
||||
add_machines=[self.machine.name] if deployed else [],
|
||||
add_groups=self.machine.deployment["sops"]["defaultGroups"],
|
||||
git_commit=False,
|
||||
)
|
||||
return secret_folder
|
||||
|
||||
def get(self, generator_name: str, name: str, shared: bool = False) -> bytes:
|
||||
return decrypt_secret(
|
||||
@@ -80,10 +105,14 @@ class SecretStore(SecretStoreBase):
|
||||
)
|
||||
(output_dir / "key.txt").write_text(key)
|
||||
|
||||
def exists_shared(self, generator_name: str, name: str) -> bool:
|
||||
secret_folder = self.secret_path(generator_name, name, shared=True)
|
||||
return (secret_folder / "secret").exists()
|
||||
|
||||
def exists(self, generator_name: str, name: str, shared: bool = False) -> bool:
|
||||
secret_folder = self.secret_path(generator_name, name, shared)
|
||||
if not (secret_folder / "secret").exists():
|
||||
return False
|
||||
# add_secret will be a no-op if the machine is already added
|
||||
add_secret(self.machine.flake_dir, self.machine.name, secret_folder)
|
||||
return True
|
||||
if not shared:
|
||||
return True
|
||||
return self.machine_has_access(generator_name, name, shared)
|
||||
|
||||
Reference in New Issue
Block a user