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 1bfe318865
commit e281b689df
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,23 +164,23 @@ 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(
test_flake: FlakeForTest,
capture_output: CaptureOutput,
age_keys: list["KeyPair"],
monkeypatch: pytest.MonkeyPatch,
) -> None:
sops_folder = test_flake.path / "sops" 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
for user, keys in user_keys.items():
key_args = [f"--age-key={key.pubkey}" for key in keys]
# add the user keys
cli.run( cli.run(
[ [
"secrets", "secrets",
@@ -187,46 +189,70 @@ def test_users(
"--flake", "--flake",
str(test_flake.path), str(test_flake.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( cli.run(["secrets", "users", "get", "--flake", str(test_flake.path), user])
["secrets", "users", "get", "--flake", str(test_flake.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.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.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.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",
@@ -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,
) )