fix: multiple user keys in secrets

We were not loading all the user keys, only the first one.
This commit is contained in:
Brian McGee
2025-04-18 17:05:47 +01:00
committed by Michael Hoang
parent 1694a977f1
commit d3e1c0b4e4
4 changed files with 106 additions and 56 deletions

View File

@@ -201,7 +201,7 @@ def encrypt_secret(
recipient_keys = collect_keys_for_path(secret_path) recipient_keys = collect_keys_for_path(secret_path)
if not admin_keys.intersection(recipient_keys): if admin_keys not in recipient_keys:
recipient_keys.update(admin_keys) recipient_keys.update(admin_keys)
files_to_commit.extend( files_to_commit.extend(

View File

@@ -360,7 +360,7 @@ def maybe_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

View File

@@ -62,6 +62,10 @@ KEYS = [
"age1eyyhln9g3cdwtrwpckugvqgtf5p8ugt0426sw38ra3wkc0t4rfhslq7txv", "age1eyyhln9g3cdwtrwpckugvqgtf5p8ugt0426sw38ra3wkc0t4rfhslq7txv",
"AGE-SECRET-KEY-1567QKA63Y9P62SHF5TCHVCT5GZX2LZ8NS0E9RKA2QHDA662SF5LQ2VJJYX", "AGE-SECRET-KEY-1567QKA63Y9P62SHF5TCHVCT5GZX2LZ8NS0E9RKA2QHDA662SF5LQ2VJJYX",
), ),
KeyPair(
"age1e9ufa6wrsr5danka50qp0np0832uz7jca7s00wyeg2nt3aqnvaks7p4jfr",
"AGE-SECRET-KEY-1Z89SHU9KAF709TTAZDARUWKC7H9TPZW4L8A2PVYSYAF7QVLCNQZQZ07U5J",
),
] ]

View File

@@ -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
@@ -162,71 +164,95 @@ def test_users(
with monkeypatch.context(): with monkeypatch.context():
_test_identities("users", test_flake, capture_output, age_keys, monkeypatch) _test_identities("users", test_flake, capture_output, age_keys, monkeypatch)
# some additional user-specific tests
admin_key = age_keys[2] def test_multiple_user_keys(
sops_folder = test_flake.path / "sops" test_flake: FlakeForTest,
capture_output: CaptureOutput,
age_keys: list["KeyPair"],
monkeypatch: pytest.MonkeyPatch,
) -> None:
sops_folder = test_flake.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]},
} }
monkeypatch.setenv("SOPS_NIX_SECRET", "deadfeed") for user, user_keys in users_keys.items():
# add the user
cli.run(
[
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
user,
*[f"--age-key={key.pubkey}" for key in user_keys],
]
)
assert (sops_folder / "users" / user / "key.json").exists()
for user, keys in user_keys.items(): # check they are returned in get
key_args = [f"--age-key={key.pubkey}" for key in keys] with capture_output as output:
cli.run(["secrets", "users", "get", "--flake", str(test_flake.path), user])
# add the user keys for user_key in user_keys:
cli.run( assert user_key.pubkey in output.out
[
"secrets", # let's do some setting and getting of secrets
"users",
"add", def random_str() -> str:
"--flake", return "".join(random.choices(string.ascii_letters, k=10))
str(test_flake.path),
user, for user_key in user_keys:
*key_args, # set a secret using each of the user's private keys
] with monkeypatch.context():
) secret_name = f"{user}_secret_{random_str()}"
assert (sops_folder / "users" / user / "key.json").exists() secret_value = random_str()
monkeypatch.setenv("SOPS_AGE_KEY", user_key.privkey)
monkeypatch.setenv("SOPS_NIX_SECRET", secret_value)
# check they are returned in get
with capture_output as output:
cli.run( cli.run(
["secrets", "users", "get", "--flake", str(test_flake.path), user] [
"secrets",
"set",
"--flake",
str(test_flake.path),
secret_name,
]
) )
for key in keys: # check the secret has each of our user's keys as a recipient
assert key.pubkey in output.out assert_secrets_file_recipients(
test_flake.path,
# set a secret
secret_name = f"{user}_secret"
cli.run(
[
"secrets",
"set",
"--flake",
str(test_flake.path),
"--user",
user,
secret_name, secret_name,
] expected_age_recipients_keypairs=[*user_keys],
) )
# check the secret has each of our user's keys as a recipient # check we can get the secret
# in addition the admin key should be there with capture_output as output:
assert_secrets_file_recipients( cli.run(
test_flake.path, ["secrets", "get", "--flake", str(test_flake.path), secret_name]
secret_name, )
expected_age_recipients_keypairs=[admin_key, *keys],
)
if len(keys) == 1: assert secret_value in output.out
continue
if len(user_keys) == 1:
continue
# 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")
# remove one of the keys
cli.run( cli.run(
[ [
"secrets", "secrets",
@@ -235,7 +261,7 @@ def test_users(
"--flake", "--flake",
str(test_flake.path), str(test_flake.path),
user, user,
keys[0].pubkey, key_to_remove.pubkey,
] ]
) )
@@ -243,7 +269,27 @@ def test_users(
assert_secrets_file_recipients( assert_secrets_file_recipients(
test_flake.path, test_flake.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.path),
user,
key_to_remove.pubkey,
]
)
# check the secret has been updated
assert_secrets_file_recipients(
test_flake.path,
secret_name,
expected_age_recipients_keypairs=user_keys,
) )