mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
Three failures of the same deploy, each uncovered by fixing the one before it.
web-app-seaweedfs-console read its two vhost domains through
lookup('config', ..., 'domains.canonical.*'), which resolves against the raw
applications tree. The onion substitution happens elsewhere -- in
get_merged_domains, where svc-net-tor's exclusive flag REPLACES the clearnet
value. On an onion node the role therefore rendered vhosts for clearnet names
while Keycloak's redirectUris, the CSP header and LOGOUT_DOMAINS all carried
the onion names, so the Filer and Master surfaces had no vhost at all and the
default server answered. Reading through lookup('domains', app).<key> is the
shape matrix and bluesky already use for named canonicals.
With the onion names in place the console's own nginx refused to start:
a server_name of ~81 characters overflows the stock bucket size of 64 and
nginx aborts with 'could not build server_names_hash'. The central openresty
has carried server_names_hash_bucket_size 128 since forever
(sys-svc-webserver-core/templates/nginx.conf.j2); the second nginx needs the
same.
With both surfaces served, an administrator still had to re-authenticate on
the Master host: cookie_domains was emitted only when the app had clearnet
domains, so an onion-exclusive node got no cookie scope and oauth2-proxy fell
back to a host-only cookie. whitelist_domains on the very next line already
handled both address families; cookie_domains now does too.
Both lists move out of the template into lookups of their own. The template
held six set-statements and two nested inline conditionals to build two JSON
arrays; the lookups take an application_id and return the list, which also
makes the behaviour testable without rendering the template. The template
structure assertion in test_cookie_scope.py went with them -- it measured
Jinja text, not behaviour -- replaced by thirteen cases over the two lookups:
single host, multi-host collapse, onion-only, dual-stack, no domains, no tor
node, and a missing DOMAIN_PRIMARY that now raises instead of silently
scoping nothing.
Evidence: make compose-playwright role=web-app-seaweedfs-console goes from
3 failed to 3 passed against the live stack. For the single-domain case the
change newly affects, web-app-prometheus keeps all four of its SSO specs
green with the scope applied.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
134 lines
4.5 KiB
Python
134 lines
4.5 KiB
Python
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
from plugins.lookup.sso_cookie_domains import LookupModule as CookieLookup
|
|
from plugins.lookup.sso_whitelist_domains import LookupModule as WhitelistLookup
|
|
|
|
ONION = "ocickgnpp2fqow4ku7juiprpdvwiq6ufv7j3jfykgnzvoe32jeasg5qd.onion"
|
|
CLEARNET = "infinito.test"
|
|
|
|
IDP = {"web-app-keycloak": ["auth." + CLEARNET]}
|
|
IDP_ONION = {"web-app-keycloak": ["auth." + ONION]}
|
|
|
|
|
|
def _loader(module):
|
|
"""Patch the lookup_loader of *module* so 'domains' and 'config' read the
|
|
scope variables the test supplies.
|
|
|
|
Args:
|
|
module: dotted path of the lookup module under test.
|
|
"""
|
|
|
|
def _get(name, *args, **kwargs):
|
|
plugin = MagicMock()
|
|
|
|
def _run(terms, variables=None, **_kwargs):
|
|
variables = variables or {}
|
|
if name == "domains":
|
|
return [variables.get("domains", {})]
|
|
return [variables.get("tor_node", "")]
|
|
|
|
plugin.run.side_effect = _run
|
|
return plugin
|
|
|
|
return patch(f"{module}.lookup_loader.get", side_effect=_get)
|
|
|
|
|
|
class TestSsoCookieDomains(unittest.TestCase):
|
|
def setUp(self):
|
|
self.lookup = CookieLookup()
|
|
patcher = _loader("plugins.lookup.sso_cookie_domains")
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def _run(self, app_id, domains):
|
|
return self.lookup.run([app_id], variables={"domains": domains})[0]
|
|
|
|
def test_single_clearnet_host_keeps_its_exact_name(self):
|
|
out = self._run("web-app-a", {"web-app-a": ["a." + CLEARNET], **IDP})
|
|
self.assertEqual(out, ["a." + CLEARNET, "auth." + CLEARNET])
|
|
|
|
def test_multi_host_app_collapses_to_the_shared_parent(self):
|
|
out = self._run(
|
|
"web-app-a",
|
|
{
|
|
"web-app-a": {
|
|
"filer": "filer.seaweedfs.s3." + CLEARNET,
|
|
"master": "master.seaweedfs.s3." + CLEARNET,
|
|
},
|
|
**IDP,
|
|
},
|
|
)
|
|
self.assertEqual(out, ["seaweedfs.s3." + CLEARNET, "auth." + CLEARNET])
|
|
|
|
def test_onion_only_app_scopes_the_onion_parent(self):
|
|
out = self._run(
|
|
"web-app-a",
|
|
{
|
|
"web-app-a": {
|
|
"filer": "filer.seaweedfs.s3." + ONION,
|
|
"master": "master.seaweedfs.s3." + ONION,
|
|
},
|
|
**IDP_ONION,
|
|
},
|
|
)
|
|
self.assertEqual(out, ["seaweedfs.s3." + ONION, "auth." + ONION])
|
|
|
|
def test_dual_stack_app_scopes_both_families(self):
|
|
out = self._run(
|
|
"web-app-a",
|
|
{"web-app-a": ["a." + CLEARNET, "a." + ONION], **IDP},
|
|
)
|
|
self.assertEqual(out, ["a." + CLEARNET, "a." + ONION, "auth." + CLEARNET])
|
|
|
|
def test_app_without_domains_yields_no_scope(self):
|
|
self.assertEqual(self._run("web-app-a", IDP), [])
|
|
|
|
def test_term_count_is_enforced(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self.lookup.run([], variables={"domains": {}})
|
|
|
|
|
|
class TestSsoWhitelistDomains(unittest.TestCase):
|
|
def setUp(self):
|
|
self.lookup = WhitelistLookup()
|
|
patcher = _loader("plugins.lookup.sso_whitelist_domains")
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def _run(self, app_id, domains, tor_node=ONION, primary=CLEARNET):
|
|
return self.lookup.run(
|
|
[app_id],
|
|
variables={
|
|
"domains": domains,
|
|
"tor_node": tor_node,
|
|
"DOMAIN_PRIMARY": primary,
|
|
},
|
|
)[0]
|
|
|
|
def test_clearnet_app_whitelists_the_primary_domain(self):
|
|
out = self._run("web-app-a", {"web-app-a": ["a." + CLEARNET]})
|
|
self.assertEqual(out, ["." + CLEARNET])
|
|
|
|
def test_onion_app_whitelists_only_the_node_onion(self):
|
|
out = self._run("web-app-a", {"web-app-a": ["a." + ONION]})
|
|
self.assertEqual(out, ["." + ONION])
|
|
|
|
def test_dual_stack_app_whitelists_both(self):
|
|
out = self._run("web-app-a", {"web-app-a": ["a." + CLEARNET, "a." + ONION]})
|
|
self.assertEqual(out, ["." + CLEARNET, "." + ONION])
|
|
|
|
def test_onion_host_without_a_provisioned_node_is_not_whitelisted(self):
|
|
out = self._run("web-app-a", {"web-app-a": ["a." + ONION]}, tor_node="")
|
|
self.assertEqual(out, [])
|
|
|
|
def test_clearnet_host_without_a_primary_domain_raises(self):
|
|
with self.assertRaises(AnsibleError):
|
|
self._run("web-app-a", {"web-app-a": ["a." + CLEARNET]}, primary="")
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
unittest.main()
|