mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 13:17:07 +00:00
Merge branch 'feature/updater' into feature/store-i18n
Brings in feature/updateraccb22da75, which carries main5995766f58. Conflict resolutions: - web-app-fider: main's tenant bootstrap (01_tenant.yml) runs first on the stack host, OIDC setup moves to 02_setup_oidc.yml, design to 03_design.yml; flat includes so main's gate test reads them. - sys-front-tls test_nginx_ssl_header: the tls fake maps the domain term store-i18n passes back to its app. - group_vars 04_webserver.yml, sys-svc-proxy CSP header: store-i18n's webserver_resource lookup plus #1256's WEBSERVER_SECURITY_HEADERS and the security_headers.lua include after the CSP block. - web-app-nextcloud: updater's decoupled Talk stack (Janus, signaling, NATS, recording builds) replaces aio-talk; store-i18n's per-network Talk backends move from the wrapper script into signaling.conf.j2 urls (NEXTCLOUD_HPB_SIGNALING_BACKEND_URLS from network_urls, canonical first); IS_COMPOSE_MODE/IS_SWARM_MODE booleans kept; the integration OpenProject digest stays delegated to STACK_HOST. - users lookup docs, token-store write_many, cache README, meta info lint: both sides combined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
5e8890d208
130 files changed
+4303
-898
No files matched your search
+187
@@ -14,12 +14,14 @@ from __future__ import annotations
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
import unittest.mock as mock
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
from utils.cache import _reset_cache_for_tests
|
||||
from utils.cache import base as cache_base
|
||||
from utils.cache import users as cache_users
|
||||
from utils.cache.carrier import USERS_RENDERED_FACT, merged_users_cache_key
|
||||
from utils.cache.users import store as cache_users_store
|
||||
from utils.cache.yaml import dump_yaml_str
|
||||
from utils.domains.default_primary import default_domain_primary
|
||||
@@ -426,6 +428,191 @@ class TestGetMergedUsersTokenSource(unittest.TestCase):
|
||||
self.assertEqual(after["foo"]["tokens"]["web-app-foo"], "minted")
|
||||
|
||||
|
||||
class _Templar:
|
||||
def __init__(self) -> None:
|
||||
self.available_variables: dict = {}
|
||||
|
||||
def template(self, value, fail_on_undefined=False):
|
||||
return value
|
||||
|
||||
|
||||
class TestGetMergedUsersCarrierFact(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
_reset_cache_for_tests()
|
||||
|
||||
@staticmethod
|
||||
def _variables(host: str = "h1") -> dict:
|
||||
return {
|
||||
"inventory_hostname": host,
|
||||
"users": {"foo": {"email": "foo@example.org"}},
|
||||
cache_users_store.STORE_FACT: {
|
||||
"foo": {"tokens": {"web-app-foo": "minted"}}
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _park(roles: Path, variables: dict, payload: dict) -> dict:
|
||||
key = merged_users_cache_key(variables, roles_dir=roles)
|
||||
variables[USERS_RENDERED_FACT] = {
|
||||
"key": [key[0], list(key[1])],
|
||||
"users": payload,
|
||||
}
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
def _merged(roles: Path, variables: dict, templar=None) -> tuple:
|
||||
with mock.patch.object(
|
||||
cache_users,
|
||||
"_render_with_templar",
|
||||
side_effect=lambda value, **_: {"rendered": value},
|
||||
) as render:
|
||||
merged = cache_users.get_merged_users(
|
||||
variables=variables,
|
||||
roles_dir=roles,
|
||||
templar=_Templar() if templar is None else templar,
|
||||
)
|
||||
return merged, render.call_count
|
||||
|
||||
def test_matching_key_serves_the_carried_payload_without_rendering(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles = _seed_minimal_user_role(Path(tmp))
|
||||
variables = self._variables()
|
||||
carried = self._park(roles, variables, {"foo": {"carried": True}})
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertIs(merged, carried)
|
||||
self.assertEqual(renders, 0)
|
||||
|
||||
def test_the_carried_payload_answers_the_rest_of_the_process(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles = _seed_minimal_user_role(Path(tmp))
|
||||
variables = self._variables()
|
||||
carried = self._park(roles, variables, {"foo": {"carried": True}})
|
||||
self._merged(roles, variables)
|
||||
del variables[USERS_RENDERED_FACT]
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertIs(merged, carried)
|
||||
self.assertEqual(renders, 0)
|
||||
|
||||
def test_stale_key_falls_back_to_a_fresh_render(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles = _seed_minimal_user_role(Path(tmp))
|
||||
variables = self._variables()
|
||||
key = merged_users_cache_key(variables, roles_dir=roles)
|
||||
variables[USERS_RENDERED_FACT] = {
|
||||
"key": [key[0], ["stale"] * len(key[1])],
|
||||
"users": {"foo": {"carried": True}},
|
||||
}
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertEqual(renders, 1)
|
||||
self.assertEqual(merged["rendered"]["foo"]["email"], "foo@example.org")
|
||||
|
||||
def test_another_hosts_carrier_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles = _seed_minimal_user_role(Path(tmp))
|
||||
variables = self._variables("h1")
|
||||
self._park(roles, variables, {"foo": {"carried": True}})
|
||||
variables["inventory_hostname"] = "h2"
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertEqual(renders, 1)
|
||||
self.assertNotIn("carried", merged["rendered"]["foo"])
|
||||
|
||||
def test_a_rotated_store_token_rejects_the_carrier(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles = _seed_minimal_user_role(Path(tmp))
|
||||
variables = self._variables()
|
||||
self._park(roles, variables, {"foo": {"carried": True}})
|
||||
variables[cache_users_store.STORE_FACT] = {
|
||||
"foo": {"tokens": {"web-app-foo": "rotated"}}
|
||||
}
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertEqual(renders, 1)
|
||||
self.assertEqual(
|
||||
merged["rendered"]["foo"]["tokens"]["web-app-foo"], "rotated"
|
||||
)
|
||||
|
||||
def _park_from_the_tokens_file(self, tmp: Path, token: str) -> tuple:
|
||||
roles = _seed_minimal_user_role(tmp)
|
||||
tokens = tmp / "tokens.yml"
|
||||
tokens.write_text(
|
||||
f"users: {{foo: {{tokens: {{web-app-foo: {token}}}}}}}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
variables = self._variables()
|
||||
del variables[cache_users_store.STORE_FACT]
|
||||
variables["FILE_TOKENS"] = str(tokens)
|
||||
carried = self._park(roles, variables, {"foo": {"carried": True}})
|
||||
return roles, variables, tokens, carried
|
||||
|
||||
def test_a_changed_tokens_file_rejects_the_carrier(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles, variables, tokens, _ = self._park_from_the_tokens_file(
|
||||
Path(tmp), "minted"
|
||||
)
|
||||
tokens.write_text(
|
||||
"users: {foo: {tokens: {web-app-foo: rotated-token}}}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertEqual(renders, 1)
|
||||
self.assertEqual(
|
||||
merged["rendered"]["foo"]["tokens"]["web-app-foo"], "rotated-token"
|
||||
)
|
||||
|
||||
def test_the_store_fact_with_the_file_tokens_keeps_the_carrier(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles, variables, _, carried = self._park_from_the_tokens_file(
|
||||
Path(tmp), "minted"
|
||||
)
|
||||
variables[cache_users_store.STORE_FACT] = {
|
||||
"foo": {"tokens": {"web-app-foo": "minted"}}
|
||||
}
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertIs(merged, carried)
|
||||
self.assertEqual(renders, 0)
|
||||
|
||||
def test_a_rotated_inventory_password_rejects_the_carrier(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles = _seed_minimal_user_role(Path(tmp))
|
||||
variables = self._variables()
|
||||
variables["users"]["foo"]["password"] = "before"
|
||||
self._park(roles, variables, {"foo": {"carried": True}})
|
||||
variables["users"] = {"foo": {"password": "after"}}
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertEqual(renders, 1)
|
||||
self.assertEqual(merged["rendered"]["foo"]["password"], "after")
|
||||
|
||||
def test_without_a_templar_the_carrier_is_ignored(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles = _seed_minimal_user_role(Path(tmp))
|
||||
variables = self._variables()
|
||||
self._park(roles, variables, {"foo": {"carried": True}})
|
||||
merged = cache_users.get_merged_users(
|
||||
variables=variables, roles_dir=roles, templar=None
|
||||
)
|
||||
self.assertNotIn("carried", merged["foo"])
|
||||
self.assertEqual(merged["foo"]["email"], "foo@example.org")
|
||||
|
||||
def test_malformed_carrier_is_ignored(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
roles = _seed_minimal_user_role(Path(tmp))
|
||||
key = merged_users_cache_key(self._variables(), roles_dir=roles)
|
||||
for carrier in (
|
||||
"not-a-mapping",
|
||||
{"key": "x", "users": {}},
|
||||
{"key": [key[0], list(key[1])], "users": "not-a-mapping"},
|
||||
{},
|
||||
):
|
||||
with self.subTest(carrier=carrier):
|
||||
_reset_cache_for_tests()
|
||||
variables = self._variables()
|
||||
variables[USERS_RENDERED_FACT] = carrier
|
||||
merged, renders = self._merged(roles, variables)
|
||||
self.assertEqual(renders, 1)
|
||||
self.assertEqual(
|
||||
merged["rendered"]["foo"]["email"], "foo@example.org"
|
||||
)
|
||||
|
||||
|
||||
class TestStoreSignature(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
_reset_cache_for_tests()
|
||||
|
||||
Reference in new issue
Block a user