Merge pull request 'age plugin support' (#3322) from feat/age-plugin-support into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3322
This commit is contained in:
@@ -1,18 +1,19 @@
|
|||||||
|
|
||||||
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
|
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
|
||||||
|
|
||||||
By default Clan utilizes the [sops](https://github.com/getsops/sops) format and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
|
By default, Clan uses the [sops](https://github.com/getsops/sops) format
|
||||||
|
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
|
||||||
Clan can also be configured to be used with other secret store [backends](https://docs.clan.lol/reference/clan-core/vars/#clan.core.vars.settings.secretStore).
|
Clan can also be configured to be used with other secret store [backends](https://docs.clan.lol/reference/clan-core/vars/#clan.core.vars.settings.secretStore).
|
||||||
|
|
||||||
This guide will walk you through:
|
This guide will walk you through:
|
||||||
|
|
||||||
- **Creating a Keypair for Your User**: Learn how to generate a keypair for $USER to securely control all secrets.
|
- **Creating a Keypair for Your User**: Learn how to generate a keypair for `$USER` to securely control all secrets.
|
||||||
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
|
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
|
||||||
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
|
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
|
||||||
|
|
||||||
## Create Your Admin Keypair
|
## Create Your Admin Keypair
|
||||||
|
|
||||||
To get started, you'll need to create **Your admin keypair**.
|
To get started, you'll need to create **your admin keypair**.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
Don't worry — if you've already made one before, this step won't change or overwrite it.
|
Don't worry — if you've already made one before, this step won't change or overwrite it.
|
||||||
@@ -34,9 +35,78 @@ Also add your age public key to the repository with 'clan secrets users add YOUR
|
|||||||
Make sure to keep a safe backup of the private key you've just created.
|
Make sure to keep a safe backup of the private key you've just created.
|
||||||
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
|
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
|
||||||
|
|
||||||
|
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
|
||||||
|
|
||||||
|
```title="~/.config/sops/age/keys.txt"
|
||||||
|
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
|
||||||
|
using `SOPS_AGE_KEY_FILE`.
|
||||||
|
For more information see the [SOPS] guide on [encrypting with age].
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
|
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
|
||||||
|
|
||||||
|
### Using Age Plugins
|
||||||
|
|
||||||
|
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
|
||||||
|
|
||||||
|
You must **precede your secret key with a comment that contains its corresponding recipient**.
|
||||||
|
|
||||||
|
This is usually output as part of the generation process
|
||||||
|
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
```title="~/.config/sops/age/keys.txt"
|
||||||
|
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
|
||||||
|
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
The comment that precedes the plugin secret key need only contain the recipient.
|
||||||
|
Any other text is ignored.
|
||||||
|
|
||||||
|
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
|
||||||
|
just `# age1zdy....`
|
||||||
|
|
||||||
|
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
|
||||||
|
are loaded when using Clan:
|
||||||
|
|
||||||
|
```nix title="flake.nix"
|
||||||
|
{
|
||||||
|
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||||
|
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, clan-core, ... }:
|
||||||
|
let
|
||||||
|
clan = clan-core.clanLib.buildClan {
|
||||||
|
inherit self;
|
||||||
|
|
||||||
|
meta.name = "myclan";
|
||||||
|
|
||||||
|
# Add Yubikey and FIDO2 HMAC plugins
|
||||||
|
# Note: the plugins listed here must be available in nixpkgs.
|
||||||
|
secrets.age.plugins = [
|
||||||
|
"age-plugin-yubikey"
|
||||||
|
"age-plugin-fido2-hmac"
|
||||||
|
];
|
||||||
|
|
||||||
|
machines = {
|
||||||
|
# elided for brevity
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit (clan) nixosConfigurations clanInternals;
|
||||||
|
|
||||||
|
# elided for brevity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Add Your Public Key(s)
|
### Add Your Public Key(s)
|
||||||
|
|
||||||
```console
|
```console
|
||||||
@@ -70,7 +140,7 @@ If you followed the quickstart tutorial all necessary secrets are initialized at
|
|||||||
You can list keys for your user with `clan secrets users get $USER`:
|
You can list keys for your user with `clan secrets users get $USER`:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
❯ bin/clan secrets users get alice
|
clan secrets users get alice
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -97,3 +167,8 @@ To remove a key from your user:
|
|||||||
```console
|
```console
|
||||||
clan secrets users remove-key $USER --age-key <your_public_key>
|
clan secrets users remove-key $USER --age-key <your_public_key>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
[age]: https://github.com/FiloSottile/age
|
||||||
|
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
|
||||||
|
[sops]: https://github.com/getsops/sops
|
||||||
|
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ in
|
|||||||
default = { };
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
secrets = lib.mkOption {
|
||||||
|
type = types.submodule { imports = [ ./secrets/interface.nix ]; };
|
||||||
|
description = ''
|
||||||
|
Secrets related options such as AGE plugins required to encrypt/decrypt secrets using the CLI.
|
||||||
|
'';
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
|
||||||
pkgsForSystem = lib.mkOption {
|
pkgsForSystem = lib.mkOption {
|
||||||
type = types.functionTo (types.nullOr types.attrs);
|
type = types.functionTo (types.nullOr types.attrs);
|
||||||
default = _system: null;
|
default = _system: null;
|
||||||
@@ -165,6 +173,7 @@ in
|
|||||||
clanModules = lib.mkOption { type = lib.types.raw; };
|
clanModules = lib.mkOption { type = lib.types.raw; };
|
||||||
source = lib.mkOption { type = lib.types.raw; };
|
source = lib.mkOption { type = lib.types.raw; };
|
||||||
meta = lib.mkOption { type = lib.types.raw; };
|
meta = lib.mkOption { type = lib.types.raw; };
|
||||||
|
secrets = lib.mkOption { type = lib.types.raw; };
|
||||||
clanLib = lib.mkOption { type = lib.types.raw; };
|
clanLib = lib.mkOption { type = lib.types.raw; };
|
||||||
all-machines-json = lib.mkOption { type = lib.types.raw; };
|
all-machines-json = lib.mkOption { type = lib.types.raw; };
|
||||||
machines = lib.mkOption { type = lib.types.raw; };
|
machines = lib.mkOption { type = lib.types.raw; };
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ in
|
|||||||
templates = config.templates;
|
templates = config.templates;
|
||||||
inventory = config.inventory;
|
inventory = config.inventory;
|
||||||
meta = config.inventory.meta;
|
meta = config.inventory.meta;
|
||||||
|
secrets = config.secrets;
|
||||||
|
|
||||||
source = "${clan-core}";
|
source = "${clan-core}";
|
||||||
|
|
||||||
|
|||||||
18
lib/build-clan/secrets/interface.nix
Normal file
18
lib/build-clan/secrets/interface.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
inherit (lib) types;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
age.plugins = lib.mkOption {
|
||||||
|
type = types.listOf (types.strMatching "age-plugin-.*");
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
A list of age plugins which must be available in the shell when encrypting and decrypting secrets.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
[
|
[
|
||||||
"age",
|
"age",
|
||||||
|
"age-plugin-fido2-hmac",
|
||||||
|
"age-plugin-ledger",
|
||||||
|
"age-plugin-se",
|
||||||
|
"age-plugin-sss",
|
||||||
|
"age-plugin-tpm",
|
||||||
|
"age-plugin-yubikey",
|
||||||
"avahi",
|
"avahi",
|
||||||
"bash",
|
"bash",
|
||||||
"bubblewrap",
|
"bubblewrap",
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ def add_group_argument(parser: argparse.ArgumentParser) -> None:
|
|||||||
|
|
||||||
def add_secret(flake_dir: Path, group: str, name: str) -> None:
|
def add_secret(flake_dir: Path, group: str, name: str) -> None:
|
||||||
secrets.allow_member(
|
secrets.allow_member(
|
||||||
|
flake_dir,
|
||||||
secrets.groups_folder(sops_secrets_folder(flake_dir) / name),
|
secrets.groups_folder(sops_secrets_folder(flake_dir) / name),
|
||||||
sops_groups_folder(flake_dir),
|
sops_groups_folder(flake_dir),
|
||||||
group,
|
group,
|
||||||
@@ -267,7 +268,7 @@ def add_secret_command(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def remove_secret(flake_dir: Path, group: str, name: str) -> None:
|
def remove_secret(flake_dir: Path, group: str, name: str) -> None:
|
||||||
updated_paths = secrets.disallow_member(
|
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(
|
commit_files(
|
||||||
updated_paths,
|
updated_paths,
|
||||||
|
|||||||
@@ -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:
|
def add_secret(flake_dir: Path, machine: str, secret_path: Path) -> None:
|
||||||
paths = secrets.allow_member(
|
paths = secrets.allow_member(
|
||||||
|
flake_dir,
|
||||||
secrets.machines_folder(secret_path),
|
secrets.machines_folder(secret_path),
|
||||||
sops_machines_folder(flake_dir),
|
sops_machines_folder(flake_dir),
|
||||||
machine,
|
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:
|
def remove_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
||||||
updated_paths = secrets.disallow_member(
|
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(
|
commit_files(
|
||||||
updated_paths,
|
updated_paths,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ def update_secrets(
|
|||||||
changed_files.extend(cleanup_dangling_symlinks(path / "machines"))
|
changed_files.extend(cleanup_dangling_symlinks(path / "machines"))
|
||||||
changed_files.extend(
|
changed_files.extend(
|
||||||
update_keys(
|
update_keys(
|
||||||
|
flake_dir,
|
||||||
path,
|
path,
|
||||||
collect_keys_for_path(path),
|
collect_keys_for_path(path),
|
||||||
)
|
)
|
||||||
@@ -155,16 +156,14 @@ def encrypt_secret(
|
|||||||
if add_users is None:
|
if add_users is None:
|
||||||
add_users = []
|
add_users = []
|
||||||
|
|
||||||
keys = sops.ensure_admin_public_key(flake_dir)
|
admin_keys = sops.ensure_admin_public_keys(flake_dir)
|
||||||
|
|
||||||
if not keys:
|
if not admin_keys:
|
||||||
# todo double check the correct command to run
|
# todo double check the correct command to run
|
||||||
msg = "No keys found. Please run 'clan secrets add-key' to add a key."
|
msg = "No keys found. Please run 'clan secrets add-key' to add a key."
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
username = next(iter(keys)).username
|
username = next(iter(admin_keys)).username
|
||||||
|
|
||||||
recipient_keys = set()
|
|
||||||
|
|
||||||
# encrypt_secret can be called before the secret has been created
|
# encrypt_secret can be called before the secret has been created
|
||||||
# so don't try to call sops.update_keys on a non-existent file:
|
# so don't try to call sops.update_keys on a non-existent file:
|
||||||
@@ -174,6 +173,7 @@ def encrypt_secret(
|
|||||||
for user in add_users:
|
for user in add_users:
|
||||||
files_to_commit.extend(
|
files_to_commit.extend(
|
||||||
allow_member(
|
allow_member(
|
||||||
|
flake_dir,
|
||||||
users_folder(secret_path),
|
users_folder(secret_path),
|
||||||
sops_users_folder(flake_dir),
|
sops_users_folder(flake_dir),
|
||||||
user,
|
user,
|
||||||
@@ -184,6 +184,7 @@ def encrypt_secret(
|
|||||||
for machine in add_machines:
|
for machine in add_machines:
|
||||||
files_to_commit.extend(
|
files_to_commit.extend(
|
||||||
allow_member(
|
allow_member(
|
||||||
|
flake_dir,
|
||||||
machines_folder(secret_path),
|
machines_folder(secret_path),
|
||||||
sops_machines_folder(flake_dir),
|
sops_machines_folder(flake_dir),
|
||||||
machine,
|
machine,
|
||||||
@@ -194,6 +195,7 @@ def encrypt_secret(
|
|||||||
for group in add_groups:
|
for group in add_groups:
|
||||||
files_to_commit.extend(
|
files_to_commit.extend(
|
||||||
allow_member(
|
allow_member(
|
||||||
|
flake_dir,
|
||||||
groups_folder(secret_path),
|
groups_folder(secret_path),
|
||||||
sops_groups_folder(flake_dir),
|
sops_groups_folder(flake_dir),
|
||||||
group,
|
group,
|
||||||
@@ -203,11 +205,12 @@ def encrypt_secret(
|
|||||||
|
|
||||||
recipient_keys = collect_keys_for_path(secret_path)
|
recipient_keys = collect_keys_for_path(secret_path)
|
||||||
|
|
||||||
if not keys.intersection(recipient_keys):
|
if admin_keys not in recipient_keys:
|
||||||
recipient_keys.update(keys)
|
recipient_keys.update(admin_keys)
|
||||||
|
|
||||||
files_to_commit.extend(
|
files_to_commit.extend(
|
||||||
allow_member(
|
allow_member(
|
||||||
|
flake_dir,
|
||||||
users_folder(secret_path),
|
users_folder(secret_path),
|
||||||
sops_users_folder(flake_dir),
|
sops_users_folder(flake_dir),
|
||||||
username,
|
username,
|
||||||
@@ -216,7 +219,7 @@ def encrypt_secret(
|
|||||||
)
|
)
|
||||||
|
|
||||||
secret_path = secret_path / "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)
|
files_to_commit.append(secret_path)
|
||||||
if git_commit:
|
if git_commit:
|
||||||
commit_files(
|
commit_files(
|
||||||
@@ -276,7 +279,11 @@ def list_directory(directory: Path) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def allow_member(
|
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]:
|
) -> list[Path]:
|
||||||
source = source_folder / name
|
source = source_folder / name
|
||||||
if not source.exists():
|
if not source.exists():
|
||||||
@@ -299,6 +306,7 @@ def allow_member(
|
|||||||
if do_update_keys:
|
if do_update_keys:
|
||||||
changed.extend(
|
changed.extend(
|
||||||
update_keys(
|
update_keys(
|
||||||
|
flake_dir,
|
||||||
group_folder.parent,
|
group_folder.parent,
|
||||||
collect_keys_for_path(group_folder.parent),
|
collect_keys_for_path(group_folder.parent),
|
||||||
)
|
)
|
||||||
@@ -306,7 +314,7 @@ def allow_member(
|
|||||||
return changed
|
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
|
target = group_folder / name
|
||||||
if not target.exists():
|
if not target.exists():
|
||||||
msg = f"{name} does not exist in group in {group_folder}: "
|
msg = f"{name} does not exist in group in {group_folder}: "
|
||||||
@@ -326,7 +334,9 @@ def disallow_member(group_folder: Path, name: str) -> list[Path]:
|
|||||||
if next(group_folder.parent.iterdir(), None) is None:
|
if next(group_folder.parent.iterdir(), None) is None:
|
||||||
group_folder.parent.rmdir()
|
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:
|
def has_secret(secret_path: Path) -> bool:
|
||||||
@@ -366,7 +376,7 @@ def decrypt_secret(flake_dir: Path, secret_path: Path) -> str:
|
|||||||
if not path.exists():
|
if not path.exists():
|
||||||
msg = f"Secret '{secret_path!s}' does not exist"
|
msg = f"Secret '{secret_path!s}' does not exist"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
return decrypt_file(path)
|
return decrypt_file(flake_dir, path)
|
||||||
|
|
||||||
|
|
||||||
def get_command(args: argparse.Namespace) -> None:
|
def get_command(args: argparse.Namespace) -> None:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
@@ -17,9 +18,11 @@ from clan_lib.api import API
|
|||||||
from clan_cli.cmd import Log, RunOpts, run
|
from clan_cli.cmd import Log, RunOpts, run
|
||||||
from clan_cli.dirs import user_config_dir
|
from clan_cli.dirs import user_config_dir
|
||||||
from clan_cli.errors import ClanError
|
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_machines_folder, sops_users_folder
|
from .folders import sops_users_folder
|
||||||
|
|
||||||
|
AGE_RECIPIENT_REGEX = re.compile(r"^.*((age1|ssh-(rsa|ed25519) ).*?)(\s|$)")
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -55,14 +58,20 @@ class KeyType(enum.Enum):
|
|||||||
def maybe_read_from_path(key_path: Path) -> None:
|
def maybe_read_from_path(key_path: Path) -> None:
|
||||||
try:
|
try:
|
||||||
# as in parse.go in age:
|
# as in parse.go in age:
|
||||||
lines = Path(key_path).read_text().strip().splitlines()
|
content = Path(key_path).read_text().strip()
|
||||||
for private_key in filter(lambda ln: not ln.startswith("#"), lines):
|
|
||||||
public_key = get_public_age_key(private_key)
|
try:
|
||||||
|
for public_key in get_public_age_keys(content):
|
||||||
log.info(
|
log.info(
|
||||||
f"Found age public key from a private key "
|
f"Found age public key from a private key "
|
||||||
f"in {key_path}: {public_key}"
|
f"in {key_path}: {public_key}"
|
||||||
)
|
)
|
||||||
|
|
||||||
keyring.append(public_key)
|
keyring.append(public_key)
|
||||||
|
except ClanError as e:
|
||||||
|
error_msg = f"Failed to read age keys from {key_path}"
|
||||||
|
raise ClanError(error_msg) from e
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return
|
return
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -72,13 +81,19 @@ class KeyType(enum.Enum):
|
|||||||
# SOPS_AGE_KEY is fed into age.ParseIdentities by Sops, and
|
# SOPS_AGE_KEY is fed into age.ParseIdentities by Sops, and
|
||||||
# reads identities line by line. See age/keysource.go in
|
# reads identities line by line. See age/keysource.go in
|
||||||
# Sops, and age/parse.go in Age.
|
# Sops, and age/parse.go in Age.
|
||||||
for private_key in keys.strip().splitlines():
|
content = keys.strip()
|
||||||
public_key = get_public_age_key(private_key)
|
|
||||||
|
try:
|
||||||
|
for public_key in get_public_age_keys(content):
|
||||||
log.info(
|
log.info(
|
||||||
f"Found age public key from a private key "
|
f"Found age public key from a private key "
|
||||||
f"in the environment (SOPS_AGE_KEY): {public_key}"
|
f"in the environment (SOPS_AGE_KEY): {public_key}"
|
||||||
)
|
)
|
||||||
|
|
||||||
keyring.append(public_key)
|
keyring.append(public_key)
|
||||||
|
except ClanError as e:
|
||||||
|
error_msg = "Failed to read age keys from SOPS_AGE_KEY"
|
||||||
|
raise ClanError(error_msg) from e
|
||||||
|
|
||||||
# Sops will try every location, see age/keysource.go
|
# Sops will try every location, see age/keysource.go
|
||||||
elif key_path := os.environ.get("SOPS_AGE_KEY_FILE"):
|
elif key_path := os.environ.get("SOPS_AGE_KEY_FILE"):
|
||||||
@@ -176,7 +191,41 @@ class Operation(enum.StrEnum):
|
|||||||
UPDATE_KEYS = "updatekeys"
|
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(
|
def sops_run(
|
||||||
|
flake_dir: str | Path,
|
||||||
call: Operation,
|
call: Operation,
|
||||||
secret_path: Path,
|
secret_path: Path,
|
||||||
public_keys: Iterable[SopsKey],
|
public_keys: Iterable[SopsKey],
|
||||||
@@ -234,7 +283,9 @@ def sops_run(
|
|||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
sops_cmd.append(str(secret_path))
|
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 = (
|
opts = (
|
||||||
dataclasses.replace(run_opts, env=environ)
|
dataclasses.replace(run_opts, env=environ)
|
||||||
if run_opts
|
if run_opts
|
||||||
@@ -249,7 +300,44 @@ def sops_run(
|
|||||||
return p.returncode, p.stdout
|
return p.returncode, p.stdout
|
||||||
|
|
||||||
|
|
||||||
def get_public_age_key(privkey: str) -> str:
|
def get_public_age_keys(contents: str) -> set[str]:
|
||||||
|
# we use a set as it's possible we may detect the same key twice, once in a `# comment` and once by recovering it
|
||||||
|
# from AGE-SECRET-KEY
|
||||||
|
keys: set[str] = set()
|
||||||
|
recipient: str | None = None
|
||||||
|
|
||||||
|
for line_number, line in enumerate(contents.splitlines()):
|
||||||
|
match = AGE_RECIPIENT_REGEX.match(line)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
recipient = match[1]
|
||||||
|
keys.add(recipient)
|
||||||
|
|
||||||
|
if line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith("AGE-PLUGIN-"):
|
||||||
|
if not recipient:
|
||||||
|
msg = f"Did you forget to precede line {line_number} with it's corresponding `# recipient: age1xxxxxxxx` entry?"
|
||||||
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
# reset recipient
|
||||||
|
recipient = None
|
||||||
|
|
||||||
|
if line.startswith("AGE-SECRET-KEY-"):
|
||||||
|
try:
|
||||||
|
keys.add(get_public_age_key_from_private_key(line))
|
||||||
|
except Exception as e:
|
||||||
|
msg = "Failed to get public key for age private key. Is the key malformed?"
|
||||||
|
raise ClanError(msg) from e
|
||||||
|
|
||||||
|
# reset recipient
|
||||||
|
recipient = None
|
||||||
|
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def get_public_age_key_from_private_key(privkey: str) -> str:
|
||||||
cmd = nix_shell(["age"], ["age-keygen", "-y"])
|
cmd = nix_shell(["age"], ["age-keygen", "-y"])
|
||||||
|
|
||||||
error_msg = "Failed to get public key for age private key. Is the key malformed?"
|
error_msg = "Failed to get public key for age private key. Is the key malformed?"
|
||||||
@@ -298,23 +386,7 @@ def get_user_name(flake_dir: Path, user: str) -> str:
|
|||||||
print(f"{flake_dir / user} already exists")
|
print(f"{flake_dir / user} already exists")
|
||||||
|
|
||||||
|
|
||||||
def maybe_get_user_or_machine(flake_dir: Path, key: SopsKey) -> set[SopsKey] | None:
|
def maybe_get_user(flake_dir: Path, key: SopsKey) -> set[SopsKey] | None:
|
||||||
folders = [sops_users_folder(flake_dir), sops_machines_folder(flake_dir)]
|
|
||||||
|
|
||||||
for folder in folders:
|
|
||||||
if folder.exists():
|
|
||||||
for user in folder.iterdir():
|
|
||||||
if not (user / "key.json").exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
keys = read_keys(user)
|
|
||||||
if key in keys:
|
|
||||||
return {SopsKey(key.pubkey, user.name, key.key_type)}
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_user(flake_dir: Path, key: SopsKey) -> set[SopsKey] | None:
|
|
||||||
folder = sops_users_folder(flake_dir)
|
folder = sops_users_folder(flake_dir)
|
||||||
|
|
||||||
if folder.exists():
|
if folder.exists():
|
||||||
@@ -324,20 +396,11 @@ def get_user(flake_dir: Path, key: SopsKey) -> set[SopsKey] | None:
|
|||||||
|
|
||||||
keys = read_keys(user)
|
keys = read_keys(user)
|
||||||
if key in keys:
|
if key in keys:
|
||||||
return {SopsKey(key.pubkey, user.name, key.key_type)}
|
return {SopsKey(key.pubkey, user.name, key.key_type) for key in keys}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
|
||||||
def ensure_user_or_machine(flake_dir: Path, key: SopsKey) -> set[SopsKey]:
|
|
||||||
maybe_keys = maybe_get_user_or_machine(flake_dir, key)
|
|
||||||
if maybe_keys:
|
|
||||||
return maybe_keys
|
|
||||||
msg = f"A sops key is not yet added to the repository. Please add it with 'clan secrets users add youruser {key.pubkey}' (replace youruser with your user name)"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def default_admin_private_key_path() -> Path:
|
def default_admin_private_key_path() -> Path:
|
||||||
raw_path = os.environ.get("SOPS_AGE_KEY_FILE")
|
raw_path = os.environ.get("SOPS_AGE_KEY_FILE")
|
||||||
if raw_path:
|
if raw_path:
|
||||||
@@ -346,8 +409,9 @@ def default_admin_private_key_path() -> Path:
|
|||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def maybe_get_admin_public_key() -> None | SopsKey:
|
def maybe_get_admin_public_key() -> SopsKey | None:
|
||||||
keyring = SopsKey.collect_public_keys()
|
keyring = SopsKey.collect_public_keys()
|
||||||
|
|
||||||
if len(keyring) == 0:
|
if len(keyring) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -366,19 +430,31 @@ def maybe_get_admin_public_key() -> None | SopsKey:
|
|||||||
return keyring[0]
|
return keyring[0]
|
||||||
|
|
||||||
|
|
||||||
def ensure_admin_public_key(flake_dir: Path) -> set[SopsKey]:
|
def ensure_admin_public_keys(flake_dir: Path) -> set[SopsKey]:
|
||||||
key = maybe_get_admin_public_key()
|
key = maybe_get_admin_public_key()
|
||||||
if key:
|
|
||||||
return ensure_user_or_machine(flake_dir, key)
|
if not key:
|
||||||
msg = "No sops key found. Please generate one with 'clan secrets key generate'."
|
msg = "No SOPS key found. Please generate one with `clan secrets key generate`."
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
user_keys = maybe_get_user(flake_dir, key)
|
||||||
|
|
||||||
def update_keys(secret_path: Path, keys: Iterable[SopsKey]) -> list[Path]:
|
if not user_keys:
|
||||||
|
# todo improve error message
|
||||||
|
msg = f"We could not figure out which Clan secrets user you are with the SOPS key we found: {key.pubkey}"
|
||||||
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
return user_keys
|
||||||
|
|
||||||
|
|
||||||
|
def update_keys(
|
||||||
|
flake_dir: str | Path, secret_path: Path, keys: Iterable[SopsKey]
|
||||||
|
) -> list[Path]:
|
||||||
secret_path = secret_path / "secret"
|
secret_path = secret_path / "secret"
|
||||||
error_msg = f"Could not update keys for {secret_path}"
|
error_msg = f"Could not update keys for {secret_path}"
|
||||||
|
|
||||||
rc, _ = sops_run(
|
rc, _ = sops_run(
|
||||||
|
flake_dir,
|
||||||
Operation.UPDATE_KEYS,
|
Operation.UPDATE_KEYS,
|
||||||
secret_path,
|
secret_path,
|
||||||
keys,
|
keys,
|
||||||
@@ -389,6 +465,7 @@ def update_keys(secret_path: Path, keys: Iterable[SopsKey]) -> list[Path]:
|
|||||||
|
|
||||||
|
|
||||||
def encrypt_file(
|
def encrypt_file(
|
||||||
|
flake_dir: str | Path,
|
||||||
secret_path: Path,
|
secret_path: Path,
|
||||||
content: str | IO[bytes] | bytes | None,
|
content: str | IO[bytes] | bytes | None,
|
||||||
pubkeys: list[SopsKey],
|
pubkeys: list[SopsKey],
|
||||||
@@ -399,6 +476,7 @@ def encrypt_file(
|
|||||||
if not content:
|
if not content:
|
||||||
# This will spawn an editor to edit the file.
|
# This will spawn an editor to edit the file.
|
||||||
rc, _ = sops_run(
|
rc, _ = sops_run(
|
||||||
|
flake_dir,
|
||||||
Operation.EDIT,
|
Operation.EDIT,
|
||||||
secret_path,
|
secret_path,
|
||||||
pubkeys,
|
pubkeys,
|
||||||
@@ -437,6 +515,7 @@ def encrypt_file(
|
|||||||
msg = f"Invalid content type: {type(content)}"
|
msg = f"Invalid content type: {type(content)}"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
sops_run(
|
sops_run(
|
||||||
|
flake_dir,
|
||||||
Operation.ENCRYPT,
|
Operation.ENCRYPT,
|
||||||
Path(source.name),
|
Path(source.name),
|
||||||
pubkeys,
|
pubkeys,
|
||||||
@@ -451,11 +530,12 @@ def encrypt_file(
|
|||||||
Path(source.name).unlink()
|
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:
|
# decryption uses private keys from the environment or default paths:
|
||||||
no_public_keys_needed: list[SopsKey] = []
|
no_public_keys_needed: list[SopsKey] = []
|
||||||
|
|
||||||
_, stdout = sops_run(
|
_, stdout = sops_run(
|
||||||
|
flake_dir,
|
||||||
Operation.DECRYPT,
|
Operation.DECRYPT,
|
||||||
secret_path,
|
secret_path,
|
||||||
no_public_keys_needed,
|
no_public_keys_needed,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
|
|
||||||
from .sops import get_public_age_key
|
from .sops import get_public_age_keys
|
||||||
|
|
||||||
VALID_SECRET_NAME = re.compile(r"^[a-zA-Z0-9._-]+$")
|
VALID_SECRET_NAME = re.compile(r"^[a-zA-Z0-9._-]+$")
|
||||||
VALID_USER_NAME = re.compile(r"^[a-z_]([a-z0-9_-]{0,31})?$")
|
VALID_USER_NAME = re.compile(r"^[a-z_]([a-z0-9_-]{0,31})?$")
|
||||||
@@ -21,14 +21,19 @@ def secret_name_type(arg_value: str) -> str:
|
|||||||
def public_or_private_age_key_type(arg_value: str) -> str:
|
def public_or_private_age_key_type(arg_value: str) -> str:
|
||||||
if Path(arg_value).is_file():
|
if Path(arg_value).is_file():
|
||||||
arg_value = Path(arg_value).read_text().strip()
|
arg_value = Path(arg_value).read_text().strip()
|
||||||
for line in arg_value.splitlines():
|
|
||||||
if line.startswith("#"):
|
public_keys = get_public_age_keys(arg_value)
|
||||||
continue
|
|
||||||
if line.startswith("age1"):
|
match len(public_keys):
|
||||||
return line.strip()
|
case 0:
|
||||||
if line.startswith("AGE-SECRET-KEY-"):
|
msg = f"Please provide an age public key starting with age1 or an age private key starting with AGE-SECRET-KEY- or AGE-PLUGIN-, got: '{arg_value}'"
|
||||||
return get_public_age_key(line)
|
raise ClanError(msg)
|
||||||
msg = f"Please provide an age public key starting with age1 or an age private key AGE-SECRET-KEY-, got: '{arg_value}'"
|
|
||||||
|
case 1:
|
||||||
|
return next(iter(public_keys))
|
||||||
|
|
||||||
|
case n:
|
||||||
|
msg = f"{n} age keys were provided, please provide only 1: '{arg_value}'"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ def list_users(flake_dir: Path) -> list[str]:
|
|||||||
|
|
||||||
def add_secret(flake_dir: Path, user: str, secret: str) -> None:
|
def add_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||||
updated_paths = secrets.allow_member(
|
updated_paths = secrets.allow_member(
|
||||||
|
flake_dir,
|
||||||
secrets.users_folder(sops_secrets_folder(flake_dir) / secret),
|
secrets.users_folder(sops_secrets_folder(flake_dir) / secret),
|
||||||
sops_users_folder(flake_dir),
|
sops_users_folder(flake_dir),
|
||||||
user,
|
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:
|
def remove_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||||
updated_paths = secrets.disallow_member(
|
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(
|
commit_files(
|
||||||
updated_paths,
|
updated_paths,
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ KEYS = [
|
|||||||
"age1eyyhln9g3cdwtrwpckugvqgtf5p8ugt0426sw38ra3wkc0t4rfhslq7txv",
|
"age1eyyhln9g3cdwtrwpckugvqgtf5p8ugt0426sw38ra3wkc0t4rfhslq7txv",
|
||||||
"AGE-SECRET-KEY-1567QKA63Y9P62SHF5TCHVCT5GZX2LZ8NS0E9RKA2QHDA662SF5LQ2VJJYX",
|
"AGE-SECRET-KEY-1567QKA63Y9P62SHF5TCHVCT5GZX2LZ8NS0E9RKA2QHDA662SF5LQ2VJJYX",
|
||||||
),
|
),
|
||||||
|
KeyPair(
|
||||||
|
"age1e9ufa6wrsr5danka50qp0np0832uz7jca7s00wyeg2nt3aqnvaks7p4jfr",
|
||||||
|
"AGE-SECRET-KEY-1Z89SHU9KAF709TTAZDARUWKC7H9TPZW4L8A2PVYSYAF7QVLCNQZQZ07U5J",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ if TYPE_CHECKING:
|
|||||||
from .age_keys import KeyPair
|
from .age_keys import KeyPair
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.with_core
|
||||||
def test_import_sops(
|
def test_import_sops(
|
||||||
test_root: Path,
|
test_root: Path,
|
||||||
test_flake: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
capture_output: CaptureOutput,
|
capture_output: CaptureOutput,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
@@ -24,7 +25,7 @@ def test_import_sops(
|
|||||||
"machines",
|
"machines",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"machine1",
|
"machine1",
|
||||||
age_keys[0].pubkey,
|
age_keys[0].pubkey,
|
||||||
]
|
]
|
||||||
@@ -35,7 +36,7 @@ def test_import_sops(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"user1",
|
"user1",
|
||||||
age_keys[1].pubkey,
|
age_keys[1].pubkey,
|
||||||
]
|
]
|
||||||
@@ -46,7 +47,7 @@ def test_import_sops(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"user2",
|
"user2",
|
||||||
age_keys[2].pubkey,
|
age_keys[2].pubkey,
|
||||||
]
|
]
|
||||||
@@ -57,7 +58,7 @@ def test_import_sops(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-user",
|
"add-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"user1",
|
"user1",
|
||||||
]
|
]
|
||||||
@@ -68,7 +69,7 @@ def test_import_sops(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-user",
|
"add-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"user2",
|
"user2",
|
||||||
]
|
]
|
||||||
@@ -80,7 +81,7 @@ def test_import_sops(
|
|||||||
"secrets",
|
"secrets",
|
||||||
"import-sops",
|
"import-sops",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"--group",
|
"--group",
|
||||||
"group1",
|
"group1",
|
||||||
"--machine",
|
"--machine",
|
||||||
@@ -90,10 +91,13 @@ def test_import_sops(
|
|||||||
|
|
||||||
cli.run(cmd)
|
cli.run(cmd)
|
||||||
with capture_output as output:
|
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())
|
users = sorted(output.out.rstrip().split())
|
||||||
assert users == ["user1", "user2"]
|
assert users == ["user1", "user2"]
|
||||||
|
|
||||||
with capture_output as output:
|
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"
|
assert output.out == "secret-value"
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ def test_add_module_to_inventory(
|
|||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
) -> None:
|
) -> None:
|
||||||
base_path = test_flake_with_core.path
|
base_path = test_flake_with_core.path
|
||||||
|
|
||||||
|
with monkeypatch.context():
|
||||||
monkeypatch.chdir(test_flake_with_core.path)
|
monkeypatch.chdir(test_flake_with_core.path)
|
||||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||||
|
|
||||||
@@ -60,7 +62,9 @@ def test_add_module_to_inventory(
|
|||||||
)
|
)
|
||||||
|
|
||||||
create_machine(opts)
|
create_machine(opts)
|
||||||
(test_flake_with_core.path / "machines" / "machine1" / "facter.json").write_text(
|
(
|
||||||
|
test_flake_with_core.path / "machines" / "machine1" / "facter.json"
|
||||||
|
).write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
@@ -87,7 +91,13 @@ def test_add_module_to_inventory(
|
|||||||
set_inventory(inventory, base_path, "Add borgbackup service")
|
set_inventory(inventory, base_path, "Add borgbackup service")
|
||||||
|
|
||||||
# cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "machine1"]
|
# cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "machine1"]
|
||||||
cmd = ["vars", "generate", "--flake", str(test_flake_with_core.path), "machine1"]
|
cmd = [
|
||||||
|
"vars",
|
||||||
|
"generate",
|
||||||
|
"--flake",
|
||||||
|
str(test_flake_with_core.path),
|
||||||
|
"machine1",
|
||||||
|
]
|
||||||
|
|
||||||
cli.run(cmd)
|
cli.run(cmd)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
|
import string
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -20,14 +22,15 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.with_core
|
||||||
def _test_identities(
|
def _test_identities(
|
||||||
what: str,
|
what: str,
|
||||||
test_flake: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
capture_output: CaptureOutput,
|
capture_output: CaptureOutput,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
sops_folder = test_flake.path / "sops"
|
sops_folder = test_flake_with_core.path / "sops"
|
||||||
|
|
||||||
what_singular = what[:-1]
|
what_singular = what[:-1]
|
||||||
test_secret_name = f"{what_singular}_secret"
|
test_secret_name = f"{what_singular}_secret"
|
||||||
@@ -43,7 +46,7 @@ def _test_identities(
|
|||||||
what,
|
what,
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"foo",
|
"foo",
|
||||||
age_keys[0].pubkey,
|
age_keys[0].pubkey,
|
||||||
]
|
]
|
||||||
@@ -56,7 +59,7 @@ def _test_identities(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin",
|
"admin",
|
||||||
admin_age_key.pubkey,
|
admin_age_key.pubkey,
|
||||||
]
|
]
|
||||||
@@ -69,28 +72,30 @@ def _test_identities(
|
|||||||
what,
|
what,
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"foo",
|
"foo",
|
||||||
age_keys[0].pubkey,
|
age_keys[0].pubkey,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
with monkeypatch.context():
|
with monkeypatch.context() as m:
|
||||||
monkeypatch.setenv("SOPS_NIX_SECRET", "deadfeed")
|
m.setenv("SOPS_NIX_SECRET", "deadfeed")
|
||||||
monkeypatch.setenv("SOPS_AGE_KEY", admin_age_key.privkey)
|
m.setenv("SOPS_AGE_KEY", admin_age_key.privkey)
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
"secrets",
|
"secrets",
|
||||||
"set",
|
"set",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
f"--{what_singular}",
|
f"--{what_singular}",
|
||||||
"foo",
|
"foo",
|
||||||
test_secret_name,
|
test_secret_name,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
test_secret_name,
|
test_secret_name,
|
||||||
expected_age_recipients_keypairs=[age_keys[0], admin_age_key],
|
expected_age_recipients_keypairs=[age_keys[0], admin_age_key],
|
||||||
)
|
)
|
||||||
@@ -103,14 +108,14 @@ def _test_identities(
|
|||||||
what,
|
what,
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"-f",
|
"-f",
|
||||||
"foo",
|
"foo",
|
||||||
age_keys[1].privkey,
|
age_keys[1].privkey,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
test_secret_name,
|
test_secret_name,
|
||||||
expected_age_recipients_keypairs=[age_keys[1], admin_age_key],
|
expected_age_recipients_keypairs=[age_keys[1], admin_age_key],
|
||||||
)
|
)
|
||||||
@@ -122,24 +127,35 @@ def _test_identities(
|
|||||||
what,
|
what,
|
||||||
"get",
|
"get",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"foo",
|
"foo",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert age_keys[1].pubkey in output.out
|
assert age_keys[1].pubkey in output.out
|
||||||
|
|
||||||
with capture_output as output:
|
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
|
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()
|
assert not (sops_folder / what / "foo" / "key.json").exists()
|
||||||
|
|
||||||
with pytest.raises(ClanError): # already removed
|
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:
|
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
|
assert "foo" not in output.out
|
||||||
|
|
||||||
user_or_machine_symlink = sops_folder / "secrets" / test_secret_name / what / "foo"
|
user_or_machine_symlink = sops_folder / "secrets" / test_secret_name / what / "foo"
|
||||||
@@ -151,112 +167,186 @@ def _test_identities(
|
|||||||
assert not user_or_machine_symlink.exists(follow_symlinks=False), err_msg
|
assert not user_or_machine_symlink.exists(follow_symlinks=False), err_msg
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.with_core
|
||||||
def test_users(
|
def test_users(
|
||||||
test_flake: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
capture_output: CaptureOutput,
|
capture_output: CaptureOutput,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
_test_identities("users", test_flake, capture_output, age_keys, monkeypatch)
|
with monkeypatch.context():
|
||||||
|
_test_identities(
|
||||||
|
"users", test_flake_with_core, capture_output, age_keys, monkeypatch
|
||||||
|
)
|
||||||
|
|
||||||
# some additional user-specific tests
|
|
||||||
|
|
||||||
admin_key = age_keys[2]
|
@pytest.mark.with_core
|
||||||
sops_folder = test_flake.path / "sops"
|
def test_multiple_user_keys(
|
||||||
|
test_flake_with_core: FlakeForTest,
|
||||||
|
capture_output: CaptureOutput,
|
||||||
|
age_keys: list["KeyPair"],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
sops_folder = test_flake_with_core.path / "sops"
|
||||||
|
|
||||||
user_keys = {
|
users_keys = {
|
||||||
"bob": [age_keys[0], age_keys[1]],
|
"bob": {age_keys[0], age_keys[1]},
|
||||||
"alice": [age_keys[2]],
|
"alice": {age_keys[2]},
|
||||||
"charlie": [age_keys[3], age_keys[4]],
|
"charlie": {age_keys[3], age_keys[4], age_keys[5]},
|
||||||
}
|
}
|
||||||
|
|
||||||
for user, keys in user_keys.items():
|
for user, user_keys in users_keys.items():
|
||||||
key_args = [f"--age-key={key.pubkey}" for key in keys]
|
# add the user
|
||||||
|
|
||||||
# add the user keys
|
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
"secrets",
|
"secrets",
|
||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
user,
|
user,
|
||||||
*key_args,
|
*[f"--age-key={key.pubkey}" for key in user_keys],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert (sops_folder / "users" / user / "key.json").exists()
|
assert (sops_folder / "users" / user / "key.json").exists()
|
||||||
|
|
||||||
# check they are returned in get
|
# check they are returned in get
|
||||||
with capture_output as output:
|
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 key in keys:
|
for user_key in user_keys:
|
||||||
assert key.pubkey in output.out
|
assert user_key.pubkey in output.out
|
||||||
|
|
||||||
|
# let's do some setting and getting of secrets
|
||||||
|
|
||||||
|
def random_str() -> str:
|
||||||
|
return "".join(random.choices(string.ascii_letters, k=10))
|
||||||
|
|
||||||
|
for user_key in user_keys:
|
||||||
|
# set a secret using each of the user's private keys
|
||||||
|
with monkeypatch.context():
|
||||||
|
secret_name = f"{user}_secret_{random_str()}"
|
||||||
|
secret_value = random_str()
|
||||||
|
|
||||||
|
monkeypatch.setenv("SOPS_AGE_KEY", user_key.privkey)
|
||||||
|
monkeypatch.setenv("SOPS_NIX_SECRET", secret_value)
|
||||||
|
|
||||||
# set a secret
|
|
||||||
secret_name = f"{user}_secret"
|
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
"secrets",
|
"secrets",
|
||||||
"set",
|
"set",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"--user",
|
|
||||||
user,
|
|
||||||
secret_name,
|
secret_name,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# check the secret has each of our user's keys as a recipient
|
# check the secret has each of our user's keys as a recipient
|
||||||
# in addition the admin key should be there
|
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
secret_name,
|
secret_name,
|
||||||
expected_age_recipients_keypairs=[admin_key, *keys],
|
expected_age_recipients_keypairs=[*user_keys],
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(keys) == 1:
|
# check we can get the secret
|
||||||
|
with capture_output as output:
|
||||||
|
cli.run(
|
||||||
|
[
|
||||||
|
"secrets",
|
||||||
|
"get",
|
||||||
|
"--flake",
|
||||||
|
str(test_flake_with_core.path),
|
||||||
|
secret_name,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert secret_value in output.out
|
||||||
|
|
||||||
|
if len(user_keys) == 1:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# remove one of the keys
|
# remove one of the user keys,
|
||||||
|
user_keys_iter = iter(user_keys)
|
||||||
|
|
||||||
|
key_to_remove = next(user_keys_iter)
|
||||||
|
key_to_encrypt_with = next(user_keys_iter)
|
||||||
|
|
||||||
|
with monkeypatch.context():
|
||||||
|
monkeypatch.setenv("SOPS_AGE_KEY", key_to_encrypt_with.privkey)
|
||||||
|
monkeypatch.setenv("SOPS_NIX_SECRET", "deafbeef")
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
"secrets",
|
"secrets",
|
||||||
"users",
|
"users",
|
||||||
"remove-key",
|
"remove-key",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
user,
|
user,
|
||||||
keys[0].pubkey,
|
key_to_remove.pubkey,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# check the secret has been updated
|
# check the secret has been updated
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
secret_name,
|
secret_name,
|
||||||
expected_age_recipients_keypairs=[admin_key, *keys[1:]],
|
expected_age_recipients_keypairs=list({*user_keys} - {key_to_remove}),
|
||||||
|
)
|
||||||
|
|
||||||
|
# add the key back
|
||||||
|
cli.run(
|
||||||
|
[
|
||||||
|
"secrets",
|
||||||
|
"users",
|
||||||
|
"add-key",
|
||||||
|
"--flake",
|
||||||
|
str(test_flake_with_core.path),
|
||||||
|
user,
|
||||||
|
key_to_remove.pubkey,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# check the secret has been updated
|
||||||
|
assert_secrets_file_recipients(
|
||||||
|
test_flake_with_core.path,
|
||||||
|
secret_name,
|
||||||
|
expected_age_recipients_keypairs=user_keys,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.with_core
|
||||||
def test_machines(
|
def test_machines(
|
||||||
test_flake: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
capture_output: CaptureOutput,
|
capture_output: CaptureOutput,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> 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(
|
def test_groups(
|
||||||
test_flake: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
capture_output: CaptureOutput,
|
capture_output: CaptureOutput,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
with capture_output as output:
|
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 == ""
|
assert output.out == ""
|
||||||
|
|
||||||
machine1_age_key = age_keys[0]
|
machine1_age_key = age_keys[0]
|
||||||
@@ -270,7 +360,7 @@ def test_groups(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-machine",
|
"add-machine",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"machine1",
|
"machine1",
|
||||||
]
|
]
|
||||||
@@ -282,7 +372,7 @@ def test_groups(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-user",
|
"add-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"groupb1",
|
"groupb1",
|
||||||
"user1",
|
"user1",
|
||||||
]
|
]
|
||||||
@@ -293,7 +383,7 @@ def test_groups(
|
|||||||
"machines",
|
"machines",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"machine1",
|
"machine1",
|
||||||
machine1_age_key.pubkey,
|
machine1_age_key.pubkey,
|
||||||
]
|
]
|
||||||
@@ -304,7 +394,7 @@ def test_groups(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-machine",
|
"add-machine",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"machine1",
|
"machine1",
|
||||||
]
|
]
|
||||||
@@ -317,7 +407,7 @@ def test_groups(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-machine",
|
"add-machine",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"machine1",
|
"machine1",
|
||||||
]
|
]
|
||||||
@@ -329,7 +419,7 @@ def test_groups(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"user1",
|
"user1",
|
||||||
user1_age_key.pubkey,
|
user1_age_key.pubkey,
|
||||||
]
|
]
|
||||||
@@ -340,7 +430,7 @@ def test_groups(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin",
|
"admin",
|
||||||
admin_age_key.pubkey,
|
admin_age_key.pubkey,
|
||||||
]
|
]
|
||||||
@@ -351,14 +441,16 @@ def test_groups(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-user",
|
"add-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"user1",
|
"user1",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
with capture_output as output:
|
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
|
out = output.out
|
||||||
assert "user1" in out
|
assert "user1" in out
|
||||||
assert "machine1" in out
|
assert "machine1" in out
|
||||||
@@ -368,12 +460,13 @@ def test_groups(
|
|||||||
with monkeypatch.context():
|
with monkeypatch.context():
|
||||||
monkeypatch.setenv("SOPS_NIX_SECRET", "deafbeef")
|
monkeypatch.setenv("SOPS_NIX_SECRET", "deafbeef")
|
||||||
monkeypatch.setenv("SOPS_AGE_KEY", admin_age_key.privkey)
|
monkeypatch.setenv("SOPS_AGE_KEY", admin_age_key.privkey)
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
"secrets",
|
"secrets",
|
||||||
"set",
|
"set",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"--group",
|
"--group",
|
||||||
"group1",
|
"group1",
|
||||||
secret_name,
|
secret_name,
|
||||||
@@ -381,7 +474,7 @@ def test_groups(
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
secret_name,
|
secret_name,
|
||||||
expected_age_recipients_keypairs=[
|
expected_age_recipients_keypairs=[
|
||||||
machine1_age_key,
|
machine1_age_key,
|
||||||
@@ -400,13 +493,13 @@ def test_groups(
|
|||||||
"groups",
|
"groups",
|
||||||
"remove-user",
|
"remove-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"user1",
|
"user1",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
secret_name,
|
secret_name,
|
||||||
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
|
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
|
||||||
err_msg=(
|
err_msg=(
|
||||||
@@ -422,13 +515,13 @@ def test_groups(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-user",
|
"add-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"user1",
|
"user1",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
secret_name,
|
secret_name,
|
||||||
expected_age_recipients_keypairs=[
|
expected_age_recipients_keypairs=[
|
||||||
machine1_age_key,
|
machine1_age_key,
|
||||||
@@ -444,12 +537,12 @@ def test_groups(
|
|||||||
"users",
|
"users",
|
||||||
"remove",
|
"remove",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"user1",
|
"user1",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
secret_name,
|
secret_name,
|
||||||
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
|
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
|
||||||
err_msg=(
|
err_msg=(
|
||||||
@@ -464,13 +557,13 @@ def test_groups(
|
|||||||
"groups",
|
"groups",
|
||||||
"remove-machine",
|
"remove-machine",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"group1",
|
"group1",
|
||||||
"machine1",
|
"machine1",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert_secrets_file_recipients(
|
assert_secrets_file_recipients(
|
||||||
test_flake.path,
|
test_flake_with_core.path,
|
||||||
secret_name,
|
secret_name,
|
||||||
expected_age_recipients_keypairs=[admin_age_key],
|
expected_age_recipients_keypairs=[admin_age_key],
|
||||||
err_msg=(
|
err_msg=(
|
||||||
@@ -479,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
|
assert first_group is None
|
||||||
|
|
||||||
# Check if the symlink to the group was removed from our foo test secret:
|
# 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 = (
|
err_msg = (
|
||||||
"Symlink to group1's key in foo secret "
|
"Symlink to group1's key in foo secret "
|
||||||
"was not cleaned up after group1 was removed"
|
"was not cleaned up after group1 was removed"
|
||||||
@@ -520,61 +613,105 @@ def use_gpg_key(key: GpgKey, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
|||||||
monkeypatch.setenv("SOPS_AGE_KEY", old_key)
|
monkeypatch.setenv("SOPS_AGE_KEY", old_key)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.with_core
|
||||||
def test_secrets(
|
def test_secrets(
|
||||||
test_flake: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
capture_output: CaptureOutput,
|
capture_output: CaptureOutput,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
gpg_key: GpgKey,
|
gpg_key: GpgKey,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
) -> None:
|
) -> None:
|
||||||
with capture_output as output:
|
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 == ""
|
assert output.out == ""
|
||||||
|
|
||||||
# Generate a new key for the clan
|
# 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:
|
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
|
assert "age private key" in output.out
|
||||||
# Read the key that was generated
|
# Read the key that was generated
|
||||||
with capture_output as output:
|
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)["publickey"]
|
|
||||||
assert key.startswith("age1")
|
key = json.loads(output.out)
|
||||||
|
assert key["publickey"].startswith("age1")
|
||||||
# Add testuser with the key that was generated for the clan
|
# Add testuser with the key that was generated for the clan
|
||||||
cli.run(
|
cli.run(
|
||||||
["secrets", "users", "add", "--flake", str(test_flake.path), "testuser", key]
|
[
|
||||||
|
"secrets",
|
||||||
|
"users",
|
||||||
|
"add",
|
||||||
|
"--flake",
|
||||||
|
str(test_flake_with_core.path),
|
||||||
|
"testuser",
|
||||||
|
key["publickey"],
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ClanError): # does not exist yet
|
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")
|
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:
|
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"
|
assert output.out == "foo"
|
||||||
with capture_output as output:
|
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")
|
users = output.out.rstrip().split("\n")
|
||||||
assert len(users) == 1, f"users: {users}"
|
assert len(users) == 1, f"users: {users}"
|
||||||
owner = users[0]
|
owner = users[0]
|
||||||
|
|
||||||
monkeypatch.setenv("EDITOR", "cat")
|
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")
|
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:
|
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"
|
assert output.out == "key\n"
|
||||||
|
|
||||||
with capture_output as output:
|
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 == ""
|
assert output.out == ""
|
||||||
|
|
||||||
with capture_output as output:
|
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"
|
assert output.out == "key\n"
|
||||||
|
|
||||||
# using the `age_keys` KeyPair, add a machine and rotate its key
|
# using the `age_keys` KeyPair, add a machine and rotate its key
|
||||||
@@ -585,7 +722,7 @@ def test_secrets(
|
|||||||
"machines",
|
"machines",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"machine1",
|
"machine1",
|
||||||
age_keys[1].pubkey,
|
age_keys[1].pubkey,
|
||||||
]
|
]
|
||||||
@@ -596,18 +733,22 @@ def test_secrets(
|
|||||||
"machines",
|
"machines",
|
||||||
"add-secret",
|
"add-secret",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"machine1",
|
"machine1",
|
||||||
"key",
|
"key",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
with capture_output as output:
|
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"
|
assert output.out == "machine1\n"
|
||||||
|
|
||||||
with use_age_key(age_keys[1].privkey, monkeypatch):
|
with use_age_key(age_keys[1].privkey, monkeypatch):
|
||||||
with capture_output as output:
|
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"
|
assert output.out == "foo"
|
||||||
|
|
||||||
# rotate machines key
|
# rotate machines key
|
||||||
@@ -617,7 +758,7 @@ def test_secrets(
|
|||||||
"machines",
|
"machines",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"-f",
|
"-f",
|
||||||
"machine1",
|
"machine1",
|
||||||
age_keys[0].privkey,
|
age_keys[0].privkey,
|
||||||
@@ -627,7 +768,9 @@ def test_secrets(
|
|||||||
# should also rotate the encrypted secret
|
# should also rotate the encrypted secret
|
||||||
with use_age_key(age_keys[0].privkey, monkeypatch):
|
with use_age_key(age_keys[0].privkey, monkeypatch):
|
||||||
with capture_output as output:
|
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"
|
assert output.out == "foo"
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
@@ -636,7 +779,7 @@ def test_secrets(
|
|||||||
"machines",
|
"machines",
|
||||||
"remove-secret",
|
"remove-secret",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"machine1",
|
"machine1",
|
||||||
"key",
|
"key",
|
||||||
]
|
]
|
||||||
@@ -648,7 +791,7 @@ def test_secrets(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"user1",
|
"user1",
|
||||||
age_keys[1].pubkey,
|
age_keys[1].pubkey,
|
||||||
]
|
]
|
||||||
@@ -659,13 +802,13 @@ def test_secrets(
|
|||||||
"users",
|
"users",
|
||||||
"add-secret",
|
"add-secret",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"user1",
|
"user1",
|
||||||
"key",
|
"key",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
with capture_output as output, use_age_key(age_keys[1].privkey, monkeypatch):
|
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"
|
assert output.out == "foo"
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
@@ -673,7 +816,7 @@ def test_secrets(
|
|||||||
"users",
|
"users",
|
||||||
"remove-secret",
|
"remove-secret",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"user1",
|
"user1",
|
||||||
"key",
|
"key",
|
||||||
]
|
]
|
||||||
@@ -686,7 +829,7 @@ def test_secrets(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-secret",
|
"add-secret",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin-group",
|
"admin-group",
|
||||||
"key",
|
"key",
|
||||||
]
|
]
|
||||||
@@ -697,7 +840,7 @@ def test_secrets(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-user",
|
"add-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin-group",
|
"admin-group",
|
||||||
"user1",
|
"user1",
|
||||||
]
|
]
|
||||||
@@ -708,7 +851,7 @@ def test_secrets(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-user",
|
"add-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin-group",
|
"admin-group",
|
||||||
owner,
|
owner,
|
||||||
]
|
]
|
||||||
@@ -719,7 +862,7 @@ def test_secrets(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-secret",
|
"add-secret",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin-group",
|
"admin-group",
|
||||||
"key",
|
"key",
|
||||||
]
|
]
|
||||||
@@ -730,7 +873,7 @@ def test_secrets(
|
|||||||
"secrets",
|
"secrets",
|
||||||
"set",
|
"set",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"--group",
|
"--group",
|
||||||
"admin-group",
|
"admin-group",
|
||||||
"key2",
|
"key2",
|
||||||
@@ -739,7 +882,9 @@ def test_secrets(
|
|||||||
|
|
||||||
with use_age_key(age_keys[1].privkey, monkeypatch):
|
with use_age_key(age_keys[1].privkey, monkeypatch):
|
||||||
with capture_output as output:
|
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"
|
assert output.out == "foo"
|
||||||
|
|
||||||
# Add an user with a GPG key
|
# Add an user with a GPG key
|
||||||
@@ -749,7 +894,7 @@ def test_secrets(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"--pgp-key",
|
"--pgp-key",
|
||||||
gpg_key.fingerprint,
|
gpg_key.fingerprint,
|
||||||
"user2",
|
"user2",
|
||||||
@@ -763,7 +908,7 @@ def test_secrets(
|
|||||||
"groups",
|
"groups",
|
||||||
"add-user",
|
"add-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin-group",
|
"admin-group",
|
||||||
"user2",
|
"user2",
|
||||||
]
|
]
|
||||||
@@ -771,7 +916,9 @@ def test_secrets(
|
|||||||
|
|
||||||
with use_gpg_key(gpg_key, monkeypatch): # user2
|
with use_gpg_key(gpg_key, monkeypatch): # user2
|
||||||
with capture_output as output:
|
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"
|
assert output.out == "foo"
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
@@ -780,7 +927,7 @@ def test_secrets(
|
|||||||
"groups",
|
"groups",
|
||||||
"remove-user",
|
"remove-user",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin-group",
|
"admin-group",
|
||||||
"user2",
|
"user2",
|
||||||
]
|
]
|
||||||
@@ -791,7 +938,7 @@ def test_secrets(
|
|||||||
capture_output as output,
|
capture_output as output,
|
||||||
):
|
):
|
||||||
# user2 is not in the group anymore
|
# 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)
|
print(output.out)
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
@@ -800,22 +947,23 @@ def test_secrets(
|
|||||||
"groups",
|
"groups",
|
||||||
"remove-secret",
|
"remove-secret",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"admin-group",
|
"admin-group",
|
||||||
"key",
|
"key",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key"])
|
cli.run(["secrets", "remove", "--flake", str(test_flake_with_core.path), "key"])
|
||||||
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key2"])
|
cli.run(["secrets", "remove", "--flake", str(test_flake_with_core.path), "key2"])
|
||||||
|
|
||||||
with capture_output as output:
|
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 == ""
|
assert output.out == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.with_core
|
||||||
def test_secrets_key_generate_gpg(
|
def test_secrets_key_generate_gpg(
|
||||||
test_flake: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
capture_output: CaptureOutput,
|
capture_output: CaptureOutput,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
gpg_key: GpgKey,
|
gpg_key: GpgKey,
|
||||||
@@ -830,14 +978,16 @@ def test_secrets_key_generate_gpg(
|
|||||||
"key",
|
"key",
|
||||||
"generate",
|
"generate",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert "age private key" not in output.out
|
assert "age private key" not in output.out
|
||||||
assert re.match(r"PGP key.+is already set", output.err) is not None
|
assert re.match(r"PGP key.+is already set", output.err) is not None
|
||||||
|
|
||||||
with capture_output as output:
|
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)
|
key = json.loads(output.out)
|
||||||
assert key["type"] == "pgp"
|
assert key["type"] == "pgp"
|
||||||
assert key["publickey"] == gpg_key.fingerprint
|
assert key["publickey"] == gpg_key.fingerprint
|
||||||
@@ -849,12 +999,13 @@ def test_secrets_key_generate_gpg(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"--pgp-key",
|
"--pgp-key",
|
||||||
gpg_key.fingerprint,
|
gpg_key.fingerprint,
|
||||||
"testuser",
|
"testuser",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
with capture_output as output:
|
with capture_output as output:
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
@@ -862,7 +1013,7 @@ def test_secrets_key_generate_gpg(
|
|||||||
"users",
|
"users",
|
||||||
"get",
|
"get",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake.path),
|
str(test_flake_with_core.path),
|
||||||
"testuser",
|
"testuser",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -873,8 +1024,26 @@ def test_secrets_key_generate_gpg(
|
|||||||
assert key["type"] == "pgp"
|
assert key["type"] == "pgp"
|
||||||
assert key["publickey"] == gpg_key.fingerprint
|
assert key["publickey"] == gpg_key.fingerprint
|
||||||
|
|
||||||
monkeypatch.setenv("SOPS_NIX_SECRET", "secret-value")
|
with monkeypatch.context() as m:
|
||||||
cli.run(["secrets", "set", "--flake", str(test_flake.path), "secret-name"])
|
m.setenv("SOPS_NIX_SECRET", "secret-value")
|
||||||
|
|
||||||
|
cli.run(
|
||||||
|
[
|
||||||
|
"secrets",
|
||||||
|
"set",
|
||||||
|
"--flake",
|
||||||
|
str(test_flake_with_core.path),
|
||||||
|
"secret-name",
|
||||||
|
]
|
||||||
|
)
|
||||||
with capture_output as output:
|
with capture_output as output:
|
||||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "secret-name"])
|
cli.run(
|
||||||
|
[
|
||||||
|
"secrets",
|
||||||
|
"get",
|
||||||
|
"--flake",
|
||||||
|
str(test_flake_with_core.path),
|
||||||
|
"secret-name",
|
||||||
|
]
|
||||||
|
)
|
||||||
assert output.out == "secret-value"
|
assert output.out == "secret-value"
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ def test_secrets_upload(
|
|||||||
config["clan"]["networking"]["targetHost"] = addr
|
config["clan"]["networking"]["targetHost"] = addr
|
||||||
config["clan"]["core"]["facts"]["secretUploadDirectory"] = str(flake.path / "facts")
|
config["clan"]["core"]["facts"]["secretUploadDirectory"] = str(flake.path / "facts")
|
||||||
flake.refresh()
|
flake.refresh()
|
||||||
|
|
||||||
|
with monkeypatch.context():
|
||||||
monkeypatch.chdir(str(flake.path))
|
monkeypatch.chdir(str(flake.path))
|
||||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||||
|
|
||||||
@@ -81,7 +83,10 @@ def test_secrets_upload(
|
|||||||
age_keys[1].pubkey,
|
age_keys[1].pubkey,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
|
||||||
|
|
||||||
cli.run(["secrets", "set", "--flake", str(flake.path), "vm1-age.key"])
|
cli.run(["secrets", "set", "--flake", str(flake.path), "vm1-age.key"])
|
||||||
|
|
||||||
flake_path = flake.path.joinpath("flake.nix")
|
flake_path = flake.path.joinpath("flake.nix")
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ def test_run(
|
|||||||
test_flake_with_core: FlakeForTest,
|
test_flake_with_core: FlakeForTest,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
with monkeypatch.context():
|
||||||
monkeypatch.chdir(test_flake_with_core.path)
|
monkeypatch.chdir(test_flake_with_core.path)
|
||||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
"secrets",
|
"secrets",
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ class SecretStore(StoreBase):
|
|||||||
|
|
||||||
for group in self.machine.deployment["sops"]["defaultGroups"]:
|
for group in self.machine.deployment["sops"]["defaultGroups"]:
|
||||||
allow_member(
|
allow_member(
|
||||||
|
self.machine.flake_dir,
|
||||||
groups_folder(secret_path),
|
groups_folder(secret_path),
|
||||||
sops_groups_folder(self.machine.flake_dir),
|
sops_groups_folder(self.machine.flake_dir),
|
||||||
group,
|
group,
|
||||||
@@ -310,6 +311,7 @@ class SecretStore(StoreBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
update_keys(
|
update_keys(
|
||||||
|
self.machine.flake_dir,
|
||||||
secret_path,
|
secret_path,
|
||||||
collect_keys_for_path(secret_path),
|
collect_keys_for_path(secret_path),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user