fix: retry mailu sync when bcrypt rejects password
This commit is contained in:
parent
04e740d3b9
commit
f332549a2d
@ -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(
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user