Files
computer-playbook/tests/unit/python/plugins/lookup/test_ca_injected.py
T
kevinveenbirkenbachandClaude Opus 5.5 69aee158f6 fix(ca_injected): answer a ca_client on the issuer it calls, and let Matrix opt in
Matrix is the only single_mode role: on a multi node it is served on onion
only, so it has no TLS of its own, yet Synapse calls the clearnet Keycloak
issuer over self-signed TLS. ca_injected returned false, Synapse got no CA,
and OIDC discovery failed with CERTIFICATE_VERIFY_FAILED, which Synapse
reports as "504 Timeout waiting for response" and Element as "This login
provider is unavailable right now".

An application with a domain and self-signed TLS of its own still gets the
CA. Otherwise a ca_client service now gets it when the SSO issuer
application resolves TLS enabled and self-signed. The cherry-picked
store-i18n rule answered a domain-less client on the global TLS_MODE; the
issuer's own resolved mode honours per-app overrides on both sides,
answers false on a tor-only node, and true when TLS_ENABLED is off but the
issuer keeps an override, or when the client serves letsencrypt itself.
Matrix declares ca_client on its matrix service.

A later merge of feature/store-i18n conflicts in ca_injected.py and its
test; keep this side.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 22:35:41 +02:00

250 lines
8.2 KiB
Python

"""Unit tests for the ca_injected lookup plugin.
Pins the predicate that decides whether the self-signed root CA is bind-mounted
into an application's containers. Anything reading the mounted file must agree
with the handler gate that creates the mount, so every term is pinned here,
including their short-circuit order.
"""
from __future__ import annotations
import importlib.util
import unittest
import unittest.mock as mock
from ansible.errors import AnsibleError
from . import PROJECT_ROOT
MODULE = "lookup_ca_injected"
ISSUER = "web-app-keycloak"
def _load_lookup():
spec = importlib.util.spec_from_file_location(
MODULE, str(PROJECT_ROOT / "plugins/lookup/ca_injected.py")
)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod
class _DummyTemplar:
def template(self, value):
return value
class TestCaInjectedLookup(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.mod = _load_lookup()
def _resolve(
self,
*,
domains,
enabled,
mode,
app="web-app-moodle",
services=None,
peer_enabled=True,
peer_mode="self_signed",
issuers=(ISSUER,),
):
calls: list[tuple[str, list]] = []
def fake_get(name, **_kwargs):
def run(args, variables=None):
calls.append((name, list(args)))
if name == "config":
return [services or {}]
if name == "domains":
return [domains]
if args[0] == app:
return [enabled if args[1] == "enabled" else mode]
if isinstance(peer_mode, Exception) and args[1] == "mode":
raise peer_mode
return [peer_enabled if args[1] == "enabled" else peer_mode]
return mock.Mock(run=run)
plugin = self.mod.LookupModule()
plugin._loader = None
plugin._templar = _DummyTemplar()
with (
mock.patch.object(self.mod.lookup_loader, "get", side_effect=fake_get),
mock.patch.object(self.mod, "issuer_applications", return_value=issuers),
):
result = plugin.run([app], variables={})
return result[0], calls
def test_a_self_signed_app_with_a_domain_gets_the_ca(self):
verdict, _ = self._resolve(
domains={"web-app-moodle": "moodle.example"},
enabled=True,
mode="self_signed",
)
self.assertIs(verdict, True)
def test_letsencrypt_does_not_get_the_ca(self):
verdict, _ = self._resolve(
domains={"web-app-moodle": "moodle.example"},
enabled=True,
mode="letsencrypt",
)
self.assertIs(verdict, False)
def test_tls_disabled_does_not_get_the_ca(self):
verdict, calls = self._resolve(
domains={"web-app-moodle": "moodle.example"},
enabled=False,
mode="self_signed",
)
self.assertIs(verdict, False)
self.assertEqual([name for name, _ in calls], ["domains", "tls", "config"])
def test_an_app_without_a_domain_never_asks_about_tls(self):
verdict, calls = self._resolve(domains={}, enabled=True, mode="self_signed")
self.assertIs(verdict, False)
self.assertEqual([name for name, _ in calls], ["domains", "config"])
def test_a_domainless_ca_client_gets_the_ca_from_a_self_signed_issuer(self):
verdict, calls = self._resolve(
domains={},
enabled=True,
mode="self_signed",
services={"agent-broker": {"ca_client": True}, "socket-proxy": {}},
)
self.assertIs(
verdict,
True,
"a container that only calls the self-signed endpoint has no domain "
"of its own, and without the CA every outbound call fails the "
"handshake",
)
self.assertEqual(
calls,
[
("domains", []),
("config", ["web-app-moodle", "services", {}]),
("tls", [ISSUER, "enabled"]),
("tls", [ISSUER, "mode"]),
],
)
def test_a_ca_client_with_a_domain_never_answers_on_the_deployment_mode(self):
verdict, calls = self._resolve(
domains={"web-app-moodle": "moodle.example"},
enabled=True,
mode="letsencrypt",
services={"agent-broker": {"ca_client": True}},
peer_mode="letsencrypt",
)
self.assertIs(
verdict,
False,
"an app that has a domain is served over its own resolved mode, which "
"may override the deployment default; the ca_client answer comes from "
"the issuer's resolved mode, never from the global TLS_MODE",
)
self.assertEqual(
[name for name, _ in calls],
["domains", "tls", "tls", "config", "tls", "tls"],
)
def test_a_ca_client_with_its_own_letsencrypt_still_trusts_a_self_signed_issuer(
self,
):
verdict, _ = self._resolve(
domains={"web-app-matrix": "matrix.example"},
enabled=True,
mode="letsencrypt",
app="web-app-matrix",
services={"matrix": {"ca_client": True}},
)
self.assertIs(
verdict,
True,
"a client served over its own letsencrypt certificate still calls the "
"issuer, whose per-app override may be self-signed",
)
def test_an_onion_only_ca_client_gets_the_ca_from_a_self_signed_issuer(self):
verdict, _ = self._resolve(
domains={"web-app-matrix": "matrix.example.onion"},
enabled=False,
mode="off",
app="web-app-matrix",
services={"matrix": {"ca_client": True}},
)
self.assertIs(
verdict,
True,
"an onion-only role has no TLS of its own but still calls the "
"self-signed clearnet issuer",
)
def test_a_ca_client_gets_nothing_when_the_issuer_serves_no_tls(self):
verdict, _ = self._resolve(
domains={"web-app-matrix": "matrix.example.onion"},
enabled=False,
mode="off",
app="web-app-matrix",
services={"matrix": {"ca_client": True}},
peer_enabled=False,
peer_mode="off",
)
self.assertIs(
verdict,
False,
"on a tor-only node the issuer is served over plain http, so there is "
"no self-signed endpoint to trust",
)
def test_a_ca_client_gets_nothing_when_the_issuer_is_not_self_signed(self):
verdict, _ = self._resolve(
domains={},
enabled=True,
mode="self_signed",
services={"agent-broker": {"ca_client": True}},
peer_mode="letsencrypt",
)
self.assertIs(verdict, False)
def test_a_ca_client_without_an_issuer_gets_nothing(self):
verdict, _ = self._resolve(
domains={},
enabled=True,
mode="self_signed",
services={"agent-broker": {"ca_client": True}},
issuers=(),
)
self.assertIs(verdict, False)
def test_a_ca_client_keeps_the_refusal_of_an_unresolvable_issuer_mode(self):
with self.assertRaises(
AnsibleError,
msg="a missing mode read as 'not self signed' withholds the CA in "
"silence, and every handshake then fails far from here",
):
self._resolve(
domains={},
enabled=True,
mode="self_signed",
services={"agent-broker": {"ca_client": True}},
peer_mode=AnsibleError("tls: required variable 'TLS_MODE' is missing"),
)
def test_the_term_is_required(self):
plugin = self.mod.LookupModule()
plugin._templar = _DummyTemplar()
with self.assertRaises(AnsibleError):
plugin.run([], variables={})
with self.assertRaises(AnsibleError):
plugin.run([" "], variables={})
if __name__ == "__main__":
unittest.main()