fix: retry mailu sync when bcrypt rejects password

This commit is contained in:
Brad Stein 2026-01-21 05:09:49 -03:00
parent 04e740d3b9
commit f332549a2d
2 changed files with 96 additions and 2 deletions

View File

@ -39,6 +39,10 @@ class MailuUserSyncResult:
updated: int = 0 updated: int = 0
skipped: int = 0 skipped: int = 0
failures: int = 0 failures: int = 0
class PasswordTooLongError(RuntimeError):
pass
mailboxes: int = 0 mailboxes: int = 0
@ -240,13 +244,34 @@ class MailuService:
mailbox_ok = False mailbox_ok = False
failed = False failed = False
rotated = False
try: try:
mailbox_ok = self._ensure_mailbox(conn, mailu_email, app_password, _display_name(user)) mailbox_ok = self._ensure_mailbox(conn, mailu_email, app_password, _display_name(user))
except PasswordTooLongError as exc:
rotated = True
app_password = random_password(24)
try:
keycloak_admin.set_user_attribute(username, MAILU_APP_PASSWORD_ATTR, app_password)
logger.info(
"mailu app password rotated",
extra={
"event": "mailu_sync",
"status": "updated",
"detail": "app password exceeded bcrypt limit",
"username": username,
},
)
mailbox_ok = self._ensure_mailbox(conn, mailu_email, app_password, _display_name(user))
except Exception as retry_exc:
self._log_sync_error(username, str(retry_exc))
failed = True
except Exception as exc: except Exception as exc:
self._log_sync_error(username, str(exc)) self._log_sync_error(username, str(exc))
failed = True failed = True
result = MailuUserSyncResult(skipped=1, updated=updated) result = MailuUserSyncResult(skipped=1, updated=updated)
if rotated:
result = MailuUserSyncResult(skipped=1, updated=max(updated, 1))
if failed: if failed:
result = MailuUserSyncResult(failures=1, updated=updated) result = MailuUserSyncResult(failures=1, updated=updated)
elif mailbox_ok: elif mailbox_ok:
@ -266,10 +291,15 @@ class MailuService:
if not _domain_matches(email): if not _domain_matches(email):
return False return False
if _password_too_long(password): if _password_too_long(password):
raise ValueError("mailu password exceeds bcrypt limit") raise PasswordTooLongError("mailu password exceeds bcrypt limit")
localpart, domain = email.split("@", 1) localpart, domain = email.split("@", 1)
hashed = bcrypt_sha256.hash(password) try:
hashed = bcrypt_sha256.hash(password)
except ValueError as exc:
if "password cannot be longer than 72 bytes" in str(exc):
raise PasswordTooLongError(str(exc)) from exc
raise
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(

View File

@ -5,6 +5,7 @@ import types
import pytest import pytest
from ariadne.services import mailu as mailu_module
from ariadne.services.firefly import FireflyService from ariadne.services.firefly import FireflyService
from ariadne.services.mailu import MailuService from ariadne.services.mailu import MailuService
from ariadne.services.nextcloud import NextcloudService from ariadne.services.nextcloud import NextcloudService
@ -451,6 +452,69 @@ def test_mailu_sync_rotates_long_password(monkeypatch) -> None:
assert mailbox_calls assert mailbox_calls
assert mailbox_calls[0][1] == "short-pass-123" assert mailbox_calls[0][1] == "short-pass-123"
def test_mailu_sync_retries_on_password_limit(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev",
mailu_db_host="localhost",
mailu_db_port=5432,
mailu_db_name="mailu",
mailu_db_user="mailu",
mailu_db_password="secret",
mailu_default_quota=20000000000,
mailu_system_users=[],
mailu_system_password="",
)
monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings)
monkeypatch.setattr("ariadne.services.mailu.keycloak_admin.ready", lambda: True)
monkeypatch.setattr(
"ariadne.services.mailu.keycloak_admin.iter_users",
lambda *args, **kwargs: [
{
"id": "1",
"username": "alice",
"enabled": True,
"email": "alice@example.com",
"attributes": {"mailu_app_password": ["short-pass"]},
"firstName": "Alice",
"lastName": "Example",
}
],
)
monkeypatch.setattr("ariadne.services.mailu.random_password", lambda *_args, **_kwargs: "retry-pass-1")
set_calls: list[tuple[str, str, str]] = []
monkeypatch.setattr(
"ariadne.services.mailu.keycloak_admin.set_user_attribute",
lambda username, key, value: set_calls.append((username, key, value)),
)
call_count = {"count": 0}
def fake_ensure(self, _conn, _email, password, _display):
call_count["count"] += 1
if call_count["count"] == 1:
raise mailu_module.PasswordTooLongError("password cannot be longer than 72 bytes")
assert password == "retry-pass-1"
return True
monkeypatch.setattr("ariadne.services.mailu.MailuService._ensure_mailbox", fake_ensure)
class DummyConn:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
monkeypatch.setattr("ariadne.services.mailu.psycopg.connect", lambda *args, **kwargs: DummyConn())
svc = MailuService()
summary = svc.sync("provision", force=True)
assert summary.processed == 1
assert call_count["count"] == 2
assert set_calls
def test_mailu_sync_skips_disabled(monkeypatch) -> None: def test_mailu_sync_skips_disabled(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace( dummy_settings = types.SimpleNamespace(
mailu_domain="bstein.dev", mailu_domain="bstein.dev",