mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 21:26:44 +00:00
fix(web-app-stalwart): authenticate bots with an app password under SSO
Under SSO the role tried to let applications relay without SMTP AUTH by widening the relay rule to the private networks. The rule compared the peer address to the text of a CIDR, which never matches, so an application could reach local mailboxes but got "550 5.1.2 Relay not allowed" for every external recipient. Repairing the comparison would have opened a relay, because published ports, Tor and the swarm mesh all hand outside clients an address inside those networks. Stalwart checks app passwords before it asks the external directory, so a bot can authenticate while the directory is OIDC. With SSO enabled the role now creates one app password per bot, acting for that account as the recovery admin, and stores it in the token store the SMTP password is already read from. A bot gets a new one when its entry is deleted in Stalwart or its token is removed. The relay rule is back to authenticated sessions only. The lookup loses its SSO relay branch and the role its submission_via_relay flag, so an SSO deployment submits on 465 with AUTH like every other one. The role's CLI test now fails when an unauthenticated session is given an external recipient, or when the bot credential is refused one. Closes #793 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ebae9c537c
commit
08ef66d8de
16 files changed
+168
-156
No files matched your search
@@ -253,7 +253,7 @@ class TestEmailLookup(unittest.TestCase):
|
||||
_write_role_config(
|
||||
self._tmp,
|
||||
"web-app-mailprov",
|
||||
{"sso": {"oidc": {"submission_via_relay": True}}},
|
||||
{"sso": {"enabled": True}},
|
||||
)
|
||||
variables = {
|
||||
"MAIL_PROVIDER": "web-app-mailprov",
|
||||
@@ -269,56 +269,11 @@ class TestEmailLookup(unittest.TestCase):
|
||||
self.assertFalse(result["start_tls"])
|
||||
self.assertFalse(result["tls"])
|
||||
|
||||
def test_sso_relay_provider_disables_auth_and_uses_port_25(self) -> None:
|
||||
# submission_via_relay + Keycloak deployed -> relay on 25, no auth, STARTTLS.
|
||||
def test_a_provider_behind_sso_keeps_authenticated_submission(self) -> None:
|
||||
_write_role_config(
|
||||
self._tmp,
|
||||
"web-app-mailprov",
|
||||
{"sso": {"oidc": {"submission_via_relay": True}}},
|
||||
)
|
||||
variables = {
|
||||
"MAIL_PROVIDER": "web-app-mailprov",
|
||||
"group_names": ["web-app-mailprov", "web-app-keycloak"],
|
||||
"groups": {"web-app-mailprov": ["host1"], "web-app-keycloak": ["host1"]},
|
||||
"TLS_ENABLED": True,
|
||||
"inventory_hostname": "host1",
|
||||
}
|
||||
result = self.lookup.run(
|
||||
[], variables=variables, roles_dir=str(self._tmp / "roles")
|
||||
)[0]
|
||||
self.assertEqual(result["port"], 25)
|
||||
self.assertFalse(result["auth"])
|
||||
self.assertTrue(result["start_tls"])
|
||||
|
||||
def test_sso_relay_inactive_without_keycloak(self) -> None:
|
||||
# Same provider, but Keycloak is not deployed: keep authenticated 465.
|
||||
_write_role_config(
|
||||
self._tmp,
|
||||
"web-app-mailprov",
|
||||
{"sso": {"oidc": {"submission_via_relay": True}}},
|
||||
)
|
||||
variables = {
|
||||
"MAIL_PROVIDER": "web-app-mailprov",
|
||||
"group_names": ["web-app-mailprov"],
|
||||
"groups": {"web-app-mailprov": ["host1"]},
|
||||
"TLS_ENABLED": True,
|
||||
"inventory_hostname": "host1",
|
||||
}
|
||||
result = self.lookup.run(
|
||||
[], variables=variables, roles_dir=str(self._tmp / "roles")
|
||||
)[0]
|
||||
self.assertEqual(result["port"], 465)
|
||||
self.assertTrue(result["auth"])
|
||||
self.assertFalse(result["start_tls"])
|
||||
|
||||
def test_sso_relay_inactive_when_sso_enabled_pinned_false(self) -> None:
|
||||
# A variant pins the provider's sso.enabled to a literal false while
|
||||
# submission_via_relay stays true (role default) and Keycloak is still
|
||||
# deployed: the provider keeps password auth, so no relay mode.
|
||||
_write_role_config(
|
||||
self._tmp,
|
||||
"web-app-mailprov",
|
||||
{"sso": {"enabled": False, "oidc": {"submission_via_relay": True}}},
|
||||
{"sso": {"enabled": True}},
|
||||
)
|
||||
variables = {
|
||||
"MAIL_PROVIDER": "web-app-mailprov",
|
||||
@@ -332,6 +287,7 @@ class TestEmailLookup(unittest.TestCase):
|
||||
)[0]
|
||||
self.assertEqual(result["port"], 465)
|
||||
self.assertTrue(result["auth"])
|
||||
self.assertEqual(result["auth_mechanism"], "on")
|
||||
self.assertFalse(result["start_tls"])
|
||||
|
||||
def test_a_provider_without_plaintext_submission_falls_back_to_its_mx(
|
||||
@@ -396,33 +352,6 @@ class TestEmailLookup(unittest.TestCase):
|
||||
self.assertTrue(result["auth"])
|
||||
self.assertEqual(result["auth_mechanism"], "plain")
|
||||
|
||||
def test_sso_relay_active_with_untemplated_enabled_gate(self) -> None:
|
||||
# The role default gates sso.enabled on group_names (raw Jinja here);
|
||||
# the guard must not treat the untemplated string as false.
|
||||
_write_role_config(
|
||||
self._tmp,
|
||||
"web-app-mailprov",
|
||||
{
|
||||
"sso": {
|
||||
"enabled": "{{ 'web-app-keycloak' in group_names }}",
|
||||
"oidc": {"submission_via_relay": True},
|
||||
}
|
||||
},
|
||||
)
|
||||
variables = {
|
||||
"MAIL_PROVIDER": "web-app-mailprov",
|
||||
"group_names": ["web-app-mailprov", "web-app-keycloak"],
|
||||
"groups": {"web-app-mailprov": ["host1"], "web-app-keycloak": ["host1"]},
|
||||
"TLS_ENABLED": True,
|
||||
"inventory_hostname": "host1",
|
||||
}
|
||||
result = self.lookup.run(
|
||||
[], variables=variables, roles_dir=str(self._tmp / "roles")
|
||||
)[0]
|
||||
self.assertEqual(result["port"], 25)
|
||||
self.assertFalse(result["auth"])
|
||||
self.assertTrue(result["start_tls"])
|
||||
|
||||
def test_computed_defaults_are_templated(self) -> None:
|
||||
self.lookup._templar = _DummyTemplar(
|
||||
{"DOMAIN_PRIMARY_RESOLVED": "mail.example.org"}
|
||||
|
||||
Reference in new issue
Block a user