fix: simplify mailu sync retry
This commit is contained in:
parent
f332549a2d
commit
06d73a5307
@ -39,11 +39,21 @@ class MailuUserSyncResult:
|
|||||||
updated: int = 0
|
updated: int = 0
|
||||||
skipped: int = 0
|
skipped: int = 0
|
||||||
failures: int = 0
|
failures: int = 0
|
||||||
|
mailboxes: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MailuSyncContext:
|
||||||
|
username: str
|
||||||
|
user_id: str
|
||||||
|
mailu_email: str
|
||||||
|
app_password: str
|
||||||
|
updated: int
|
||||||
|
display_name: str
|
||||||
|
|
||||||
|
|
||||||
class PasswordTooLongError(RuntimeError):
|
class PasswordTooLongError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
mailboxes: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -216,14 +226,17 @@ class MailuService:
|
|||||||
return True
|
return True
|
||||||
return self._is_service_account(user, username)
|
return self._is_service_account(user, username)
|
||||||
|
|
||||||
def _sync_user(self, conn: psycopg.Connection, user: dict[str, Any]) -> MailuUserSyncResult:
|
def _build_sync_context(
|
||||||
|
self,
|
||||||
|
user: dict[str, Any],
|
||||||
|
) -> tuple[MailuSyncContext | None, MailuUserSyncResult | None]:
|
||||||
username = self._username(user)
|
username = self._username(user)
|
||||||
if self._should_skip_user(user, username):
|
if self._should_skip_user(user, username):
|
||||||
return MailuUserSyncResult(skipped=1)
|
return None, MailuUserSyncResult(skipped=1)
|
||||||
|
|
||||||
user_id = self._user_id(user)
|
user_id = self._user_id(user)
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return MailuUserSyncResult(failures=1)
|
return None, MailuUserSyncResult(failures=1)
|
||||||
|
|
||||||
attrs = user.get("attributes")
|
attrs = user.get("attributes")
|
||||||
if not isinstance(attrs, dict):
|
if not isinstance(attrs, dict):
|
||||||
@ -237,46 +250,79 @@ class MailuService:
|
|||||||
enabled, updates, app_password = self._prepare_updates(username, attrs, mailu_email)
|
enabled, updates, app_password = self._prepare_updates(username, attrs, mailu_email)
|
||||||
|
|
||||||
if not enabled:
|
if not enabled:
|
||||||
return MailuUserSyncResult(skipped=1)
|
return None, MailuUserSyncResult(skipped=1)
|
||||||
if not self._apply_updates(user_id, updates, username):
|
if not self._apply_updates(user_id, updates, username):
|
||||||
return MailuUserSyncResult(failures=1)
|
return None, MailuUserSyncResult(failures=1)
|
||||||
updated = 1 if updates else 0
|
updated = 1 if updates else 0
|
||||||
|
display_name = _display_name(user)
|
||||||
|
|
||||||
|
return (
|
||||||
|
MailuSyncContext(
|
||||||
|
username=username,
|
||||||
|
user_id=user_id,
|
||||||
|
mailu_email=mailu_email,
|
||||||
|
app_password=app_password,
|
||||||
|
updated=updated,
|
||||||
|
display_name=display_name,
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _ensure_mailbox_with_retry(
|
||||||
|
self,
|
||||||
|
conn: psycopg.Connection,
|
||||||
|
ctx: MailuSyncContext,
|
||||||
|
) -> tuple[bool, bool, bool]:
|
||||||
mailbox_ok = False
|
mailbox_ok = False
|
||||||
failed = False
|
|
||||||
rotated = False
|
rotated = False
|
||||||
|
failed = False
|
||||||
try:
|
try:
|
||||||
mailbox_ok = self._ensure_mailbox(conn, mailu_email, app_password, _display_name(user))
|
mailbox_ok = self._ensure_mailbox(conn, ctx.mailu_email, ctx.app_password, ctx.display_name)
|
||||||
except PasswordTooLongError as exc:
|
except PasswordTooLongError:
|
||||||
rotated = True
|
rotated = True
|
||||||
app_password = random_password(24)
|
app_password = random_password(24)
|
||||||
try:
|
try:
|
||||||
keycloak_admin.set_user_attribute(username, MAILU_APP_PASSWORD_ATTR, app_password)
|
keycloak_admin.set_user_attribute(ctx.username, MAILU_APP_PASSWORD_ATTR, app_password)
|
||||||
logger.info(
|
logger.info(
|
||||||
"mailu app password rotated",
|
"mailu app password rotated",
|
||||||
extra={
|
extra={
|
||||||
"event": "mailu_sync",
|
"event": "mailu_sync",
|
||||||
"status": "updated",
|
"status": "updated",
|
||||||
"detail": "app password exceeded bcrypt limit",
|
"detail": "app password exceeded bcrypt limit",
|
||||||
"username": username,
|
"username": ctx.username,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
mailbox_ok = self._ensure_mailbox(conn, mailu_email, app_password, _display_name(user))
|
mailbox_ok = self._ensure_mailbox(conn, ctx.mailu_email, app_password, ctx.display_name)
|
||||||
except Exception as retry_exc:
|
except Exception as retry_exc:
|
||||||
self._log_sync_error(username, str(retry_exc))
|
self._log_sync_error(ctx.username, str(retry_exc))
|
||||||
failed = True
|
failed = True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._log_sync_error(username, str(exc))
|
self._log_sync_error(ctx.username, str(exc))
|
||||||
failed = True
|
failed = True
|
||||||
|
|
||||||
result = MailuUserSyncResult(skipped=1, updated=updated)
|
return mailbox_ok, failed, rotated
|
||||||
if rotated:
|
|
||||||
result = MailuUserSyncResult(skipped=1, updated=max(updated, 1))
|
@staticmethod
|
||||||
|
def _build_sync_result(
|
||||||
|
updated: int,
|
||||||
|
mailbox_ok: bool,
|
||||||
|
failed: bool,
|
||||||
|
rotated: bool,
|
||||||
|
) -> MailuUserSyncResult:
|
||||||
if failed:
|
if failed:
|
||||||
result = MailuUserSyncResult(failures=1, updated=updated)
|
return MailuUserSyncResult(failures=1, updated=updated)
|
||||||
elif mailbox_ok:
|
if mailbox_ok:
|
||||||
result = MailuUserSyncResult(processed=1, updated=updated, mailboxes=1)
|
return MailuUserSyncResult(processed=1, updated=updated, mailboxes=1)
|
||||||
return result
|
if rotated:
|
||||||
|
return MailuUserSyncResult(skipped=1, updated=max(updated, 1))
|
||||||
|
return MailuUserSyncResult(skipped=1, updated=updated)
|
||||||
|
|
||||||
|
def _sync_user(self, conn: psycopg.Connection, user: dict[str, Any]) -> MailuUserSyncResult:
|
||||||
|
ctx, early = self._build_sync_context(user)
|
||||||
|
if early is not None:
|
||||||
|
return early
|
||||||
|
mailbox_ok, failed, rotated = self._ensure_mailbox_with_retry(conn, ctx)
|
||||||
|
return self._build_sync_result(ctx.updated, mailbox_ok, failed, rotated)
|
||||||
|
|
||||||
def _ensure_mailbox(
|
def _ensure_mailbox(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -466,9 +466,16 @@ def test_mailu_sync_retries_on_password_limit(monkeypatch) -> None:
|
|||||||
mailu_system_password="",
|
mailu_system_password="",
|
||||||
)
|
)
|
||||||
monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings)
|
monkeypatch.setattr("ariadne.services.mailu.settings", dummy_settings)
|
||||||
monkeypatch.setattr("ariadne.services.mailu.keycloak_admin.ready", lambda: True)
|
monkeypatch.setattr(mailu_module.keycloak_admin, "ready", lambda: True)
|
||||||
|
update_calls: list[tuple[str, dict[str, object]]] = []
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"ariadne.services.mailu.keycloak_admin.iter_users",
|
mailu_module.keycloak_admin,
|
||||||
|
"update_user_safe",
|
||||||
|
lambda user_id, payload: update_calls.append((user_id, payload)),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
mailu_module.keycloak_admin,
|
||||||
|
"iter_users",
|
||||||
lambda *args, **kwargs: [
|
lambda *args, **kwargs: [
|
||||||
{
|
{
|
||||||
"id": "1",
|
"id": "1",
|
||||||
@ -485,7 +492,8 @@ def test_mailu_sync_retries_on_password_limit(monkeypatch) -> None:
|
|||||||
|
|
||||||
set_calls: list[tuple[str, str, str]] = []
|
set_calls: list[tuple[str, str, str]] = []
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"ariadne.services.mailu.keycloak_admin.set_user_attribute",
|
mailu_module.keycloak_admin,
|
||||||
|
"set_user_attribute",
|
||||||
lambda username, key, value: set_calls.append((username, key, value)),
|
lambda username, key, value: set_calls.append((username, key, value)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -513,8 +521,11 @@ def test_mailu_sync_retries_on_password_limit(monkeypatch) -> None:
|
|||||||
summary = svc.sync("provision", force=True)
|
summary = svc.sync("provision", force=True)
|
||||||
|
|
||||||
assert summary.processed == 1
|
assert summary.processed == 1
|
||||||
|
assert update_calls
|
||||||
assert call_count["count"] == 2
|
assert call_count["count"] == 2
|
||||||
assert set_calls
|
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