feat(clan-cli): support multiple keys for a user

This commit is contained in:
Brian McGee
2025-04-02 16:08:42 +01:00
committed by Mic92
parent b8e33babec
commit aa4fe27e51
10 changed files with 426 additions and 112 deletions

View File

@@ -1,5 +1,7 @@
import dataclasses
import json
import os
from collections.abc import Iterable
from pathlib import Path
import pytest
@@ -7,18 +9,18 @@ from clan_cli.secrets.folders import sops_secrets_folder
from clan_cli.tests.helpers import cli
@dataclasses.dataclass(frozen=True)
class KeyPair:
def __init__(self, pubkey: str, privkey: str) -> None:
self.pubkey = pubkey
self.privkey = privkey
pubkey: str
privkey: str
class SopsSetup:
"""Hold a list of three key pairs and create an "admin" user in the clan.
"""Hold a list of key pairs and create an "admin" user in the clan.
The first key in the list is used as the admin key and
the private part of the key is exposed in the
`SOPS_AGE_KEY` environment variable, the two others can
`SOPS_AGE_KEY` environment variable, the others can
be used to add machines or other users.
"""
@@ -52,6 +54,14 @@ KEYS = [
"age1dhuh9xtefhgpr2sjjf7gmp9q2pr37z92rv4wsadxuqdx48989g7qj552qp",
"AGE-SECRET-KEY-169N3FT32VNYQ9WYJMLUSVTMA0TTZGVJF7YZWS8AHTWJ5RR9VGR7QCD8SKF",
),
KeyPair(
"age1n58rxm8y6h9prmwn0qk7nggfsu9f9j4u35dxg7akpkjd5vgsavssfzmq9y",
"AGE-SECRET-KEY-1YU2JVE445KT6S8UN3403NHH6EZU404RMEH9RTME9SPWXWMLJS0LQM5NWM7",
),
KeyPair(
"age1eyyhln9g3cdwtrwpckugvqgtf5p8ugt0426sw38ra3wkc0t4rfhslq7txv",
"AGE-SECRET-KEY-1567QKA63Y9P62SHF5TCHVCT5GZX2LZ8NS0E9RKA2QHDA662SF5LQ2VJJYX",
),
]
@@ -73,7 +83,7 @@ def sops_setup(
def assert_secrets_file_recipients(
flake_path: Path,
secret_name: str,
expected_age_recipients_keypairs: list["KeyPair"],
expected_age_recipients_keypairs: Iterable["KeyPair"],
err_msg: str | None = None,
) -> None:
"""Checks that the recipients of a secrets file matches expectations.

View File

@@ -59,7 +59,7 @@ def test_machine_delete(
) -> None:
flake = flake_with_sops
admin_key, machine_key, machine2_key = sops_setup.keys
admin_key, machine_key, machine2_key, *xs = sops_setup.keys
# create a couple machines with their keys
for name, key in (("my-machine", machine_key), ("my-machine2", machine2_key)):

View File

@@ -159,6 +159,86 @@ def test_users(
) -> None:
_test_identities("users", test_flake, capture_output, age_keys, monkeypatch)
# some additional user-specific tests
admin_key = age_keys[2]
sops_folder = test_flake.path / "sops"
user_keys = {
"bob": [age_keys[0], age_keys[1]],
"alice": [age_keys[2]],
"charlie": [age_keys[3], age_keys[4]],
}
for user, keys in user_keys.items():
key_args = [f"--age-key={key.pubkey}" for key in keys]
# add the user keys
cli.run(
[
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
user,
*key_args,
]
)
assert (sops_folder / "users" / user / "key.json").exists()
# check they are returned in get
with capture_output as output:
cli.run(["secrets", "users", "get", "--flake", str(test_flake.path), user])
for key in keys:
assert key.pubkey in output.out
# set a secret
secret_name = f"{user}_secret"
cli.run(
[
"secrets",
"set",
"--flake",
str(test_flake.path),
"--user",
user,
secret_name,
]
)
# 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(
test_flake.path,
secret_name,
expected_age_recipients_keypairs=[admin_key, *keys],
)
if len(keys) == 1:
continue
# remove one of the keys
cli.run(
[
"secrets",
"users",
"remove-key",
"--flake",
str(test_flake.path),
user,
keys[0].pubkey,
]
)
# check the secret has been updated
assert_secrets_file_recipients(
test_flake.path,
secret_name,
expected_age_recipients_keypairs=[admin_key, *keys[1:]],
)
def test_machines(
test_flake: FlakeForTest,
@@ -786,7 +866,10 @@ def test_secrets_key_generate_gpg(
"testuser",
]
)
key = json.loads(output.out)
keys = json.loads(output.out)
assert len(keys) == 1
key = keys[0]
assert key["type"] == "pgp"
assert key["publickey"] == gpg_key.fingerprint