Merge pull request 'vars password-store: fix secret mangling due to string encoding' (#4227) from pass-fix-bytes into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4227
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import tarfile
|
import tarfile
|
||||||
|
import subprocess
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
@@ -8,7 +9,6 @@ from tempfile import TemporaryDirectory
|
|||||||
from clan_cli.ssh.upload import upload
|
from clan_cli.ssh.upload import upload
|
||||||
from clan_cli.vars._types import StoreBase
|
from clan_cli.vars._types import StoreBase
|
||||||
from clan_cli.vars.generate import Generator, Var
|
from clan_cli.vars.generate import Generator, Var
|
||||||
from clan_lib.cmd import CmdOut, Log, RunOpts, run
|
|
||||||
from clan_lib.machines.machines import Machine
|
from clan_lib.machines.machines import Machine
|
||||||
from clan_lib.ssh.remote import Remote
|
from clan_lib.ssh.remote import Remote
|
||||||
|
|
||||||
@@ -33,13 +33,11 @@ class SecretStore(StoreBase):
|
|||||||
def store_dir(self) -> Path:
|
def store_dir(self) -> Path:
|
||||||
"""Get the password store directory, cached after first access."""
|
"""Get the password store directory, cached after first access."""
|
||||||
if self._store_dir is None:
|
if self._store_dir is None:
|
||||||
result = self._run_pass(
|
result = self._run_pass("git", "rev-parse", "--show-toplevel", check=False)
|
||||||
"git", "rev-parse", "--show-toplevel", options=RunOpts(check=False)
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
msg = "Password store must be a git repository"
|
msg = "Password store must be a git repository"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
self._store_dir = Path(result.stdout.strip())
|
self._store_dir = Path(result.stdout.strip().decode())
|
||||||
return self._store_dir
|
return self._store_dir
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -79,9 +77,20 @@ class SecretStore(StoreBase):
|
|||||||
def entry_dir(self, generator: Generator, name: str) -> Path:
|
def entry_dir(self, generator: Generator, name: str) -> Path:
|
||||||
return Path(self.entry_prefix) / self.rel_dir(generator, name)
|
return Path(self.entry_prefix) / self.rel_dir(generator, name)
|
||||||
|
|
||||||
def _run_pass(self, *args: str, options: RunOpts | None = None) -> CmdOut:
|
def _run_pass(
|
||||||
|
self, *args: str, input: bytes | None = None, check: bool = True
|
||||||
|
) -> subprocess.CompletedProcess[bytes]:
|
||||||
cmd = [self._pass_command, *args]
|
cmd = [self._pass_command, *args]
|
||||||
return run(cmd, options)
|
# We need bytes support here, so we can not use clan cmd.
|
||||||
|
# If you change this to run( add bytes support to it first!
|
||||||
|
# otherwise we mangle binary secrets (which is annoying to debug)
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
input=input,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
check=check,
|
||||||
|
)
|
||||||
|
|
||||||
def _set(
|
def _set(
|
||||||
self,
|
self,
|
||||||
@@ -90,12 +99,12 @@ class SecretStore(StoreBase):
|
|||||||
value: bytes,
|
value: bytes,
|
||||||
) -> Path | None:
|
) -> Path | None:
|
||||||
pass_call = ["insert", "-m", str(self.entry_dir(generator, var.name))]
|
pass_call = ["insert", "-m", str(self.entry_dir(generator, var.name))]
|
||||||
self._run_pass(*pass_call, options=RunOpts(input=value, check=True))
|
self._run_pass(*pass_call, input=value, check=True)
|
||||||
return None # we manage the files outside of the git repo
|
return None # we manage the files outside of the git repo
|
||||||
|
|
||||||
def get(self, generator: Generator, name: str) -> bytes:
|
def get(self, generator: Generator, name: str) -> bytes:
|
||||||
pass_name = str(self.entry_dir(generator, name))
|
pass_name = str(self.entry_dir(generator, name))
|
||||||
return self._run_pass("show", pass_name).stdout.encode()
|
return self._run_pass("show", pass_name).stdout
|
||||||
|
|
||||||
def exists(self, generator: Generator, name: str) -> bool:
|
def exists(self, generator: Generator, name: str) -> bool:
|
||||||
pass_name = str(self.entry_dir(generator, name))
|
pass_name = str(self.entry_dir(generator, name))
|
||||||
@@ -106,33 +115,22 @@ class SecretStore(StoreBase):
|
|||||||
|
|
||||||
def delete(self, generator: Generator, name: str) -> Iterable[Path]:
|
def delete(self, generator: Generator, name: str) -> Iterable[Path]:
|
||||||
pass_name = str(self.entry_dir(generator, name))
|
pass_name = str(self.entry_dir(generator, name))
|
||||||
self._run_pass("rm", "--force", pass_name, options=RunOpts(check=True))
|
self._run_pass("rm", "--force", pass_name, check=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def delete_store(self) -> Iterable[Path]:
|
def delete_store(self) -> Iterable[Path]:
|
||||||
machine_dir = Path(self.entry_prefix) / "per-machine" / self.machine.name
|
machine_dir = Path(self.entry_prefix) / "per-machine" / self.machine.name
|
||||||
# Check if the directory exists in the password store before trying to delete
|
# Check if the directory exists in the password store before trying to delete
|
||||||
result = self._run_pass("ls", str(machine_dir), options=RunOpts(check=False))
|
result = self._run_pass("ls", str(machine_dir), check=False)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
self._run_pass(
|
self._run_pass("rm", "--force", "--recursive", str(machine_dir), check=True)
|
||||||
"rm",
|
|
||||||
"--force",
|
|
||||||
"--recursive",
|
|
||||||
str(machine_dir),
|
|
||||||
options=RunOpts(check=True),
|
|
||||||
)
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def generate_hash(self) -> bytes:
|
def generate_hash(self) -> bytes:
|
||||||
result = self._run_pass(
|
result = self._run_pass(
|
||||||
"git",
|
"git", "log", "-1", "--format=%H", self.entry_prefix, check=False
|
||||||
"log",
|
|
||||||
"-1",
|
|
||||||
"--format=%H",
|
|
||||||
self.entry_prefix,
|
|
||||||
options=RunOpts(check=False),
|
|
||||||
)
|
)
|
||||||
git_hash = result.stdout.strip().encode()
|
git_hash = result.stdout.strip()
|
||||||
|
|
||||||
if not git_hash:
|
if not git_hash:
|
||||||
return b""
|
return b""
|
||||||
@@ -155,6 +153,8 @@ class SecretStore(StoreBase):
|
|||||||
if not local_hash:
|
if not local_hash:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
from clan_lib.cmd import RunOpts, Log
|
||||||
|
|
||||||
remote_hash = host.run(
|
remote_hash = host.run(
|
||||||
[
|
[
|
||||||
"cat",
|
"cat",
|
||||||
@@ -166,7 +166,7 @@ class SecretStore(StoreBase):
|
|||||||
if not remote_hash:
|
if not remote_hash:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return local_hash.decode() != remote_hash
|
return local_hash != remote_hash.encode()
|
||||||
|
|
||||||
def populate_dir(self, output_dir: Path, phases: list[str]) -> None:
|
def populate_dir(self, output_dir: Path, phases: list[str]) -> None:
|
||||||
from clan_cli.vars.generate import Generator
|
from clan_cli.vars.generate import Generator
|
||||||
|
|||||||
Reference in New Issue
Block a user