diff --git a/.env.example b/.env.example index 734c42e64..92d06d130 100644 --- a/.env.example +++ b/.env.example @@ -89,6 +89,15 @@ AUTH_LDAP_USER_SEARCH=ou=Pracownicy,dc=auth,dc=local # Bezpieczny do ujawnienia w przeglądarce. Pusty = front-end Rollbar wyłączony. ROLLBAR_CLIENT_ACCESS_TOKEN= +# Wycisza w Rollbarze smtplib.SMTPAuthenticationError (i TYLKO ją). Do włączenia +# na instalacji, gdzie administratorzy poczty klienta mają znaną, zgłoszoną +# awarię po swojej stronie. Ustawić także dla celery workera — wyjątek leci +# z djcelery_email, nie z requestu. +# NIE wycisza SMTPRecipientsRefused ani SMTPSenderRefused: te wskazują na NASZE +# dane/konfigurację. Przy DJANGO_BPP_HOSTNAMES działa na wszystkie uczelnie +# obsługiwane przez dany proces. +# DJANGO_BPP_ROLLBAR_IGNORE_SMTP_AUTH_ERRORS=1 + # # Docker exposed ports (defaults — odkomentuj, żeby zmienić) # diff --git a/src/bpp/newsfragments/+rollbar-wycisz-bledy-poczty.feature.rst b/src/bpp/newsfragments/+rollbar-wycisz-bledy-poczty.feature.rst new file mode 100644 index 000000000..9269e6008 --- /dev/null +++ b/src/bpp/newsfragments/+rollbar-wycisz-bledy-poczty.feature.rst @@ -0,0 +1,6 @@ +Nowa zmienna środowiskowa ``DJANGO_BPP_ROLLBAR_IGNORE_SMTP_AUTH_ERRORS`` +(domyślnie wyłączona) pozwala wyciszyć w monitoringu błędów zgłoszenia +o odrzuconych poświadczeniach serwera poczty na konkretnej instalacji. +Do użycia tam, gdzie administratorzy poczty po stronie klienta mają znaną +awarię, której nie da się naprawić po naszej stronie. Pozostałe błędy poczty +— w tym błędny adres odbiorcy czy nadawcy — są raportowane jak dotąd. diff --git a/src/django_bpp/rollbar_filters.py b/src/django_bpp/rollbar_filters.py new file mode 100644 index 000000000..bcdf97351 --- /dev/null +++ b/src/django_bpp/rollbar_filters.py @@ -0,0 +1,46 @@ +"""Filtry poziomów wyjątków dla Rollbara (``exception_level_filters``). + +pyrollbar pozwala przypisać klasie wyjątku poziom ``"ignored"`` — taki wyjątek +nie jest w ogóle wysyłany. Używamy tego wyłącznie tam, gdzie awaria jest znana, +zgłoszona i leży POZA naszym kodem, więc raport nie niesie już informacji. + +Dlaczego kodem, a nie „mute" w interfejsie Rollbara: mute jest per item, a item +przepada przy zmianie hasha — wystarczyła podbitka pyrollbara 1.3.0 → 1.4.0, +żeby ten sam błąd SMTP odszczepił się w drugi item (#379 → #1554). Wyciszenie +w kodzie jest trwałe i, co ważniejsze, WIDOCZNE w repozytorium: da się je +znaleźć gitem i cofnąć, gdy klient naprawi pocztę. + +Wyodrębnione z ``settings.base`` jako czysta funkcja, żeby dało się to +przetestować bez przeładowywania ustawień Django. +""" + +from __future__ import annotations + +import smtplib + + +def zbuduj_exception_level_filters( + *, ignoruj_bledy_uwierzytelniania_smtp: bool = False +) -> list[tuple[type[BaseException], str]]: + """Zwraca listę par ``(klasa_wyjątku, poziom)`` dla ``ROLLBAR``. + + :param ignoruj_bledy_uwierzytelniania_smtp: wycisza ``SMTPAuthenticationError`` + — i TYLKO ją. Włączane per instalacja przez + ``DJANGO_BPP_ROLLBAR_IGNORE_SMTP_AUTH_ERRORS``, domyślnie WYŁĄCZONE. + + Celowo NIE wyciszamy całej rodziny ``smtplib.SMTPException``: + ``SMTPRecipientsRefused`` to zły adres w NASZEJ bazie, a + ``SMTPSenderRefused`` to nasza konfiguracja ``DEFAULT_FROM_EMAIL`` — + jedno i drugie jest do naprawienia po naszej stronie i chcemy o tym + wiedzieć także na instalacji z zepsutą pocztą. + + UWAGA: to ustawienie działa na CAŁY PROCES. Przy konfiguracji + wielotenantowej (``DJANGO_BPP_HOSTNAMES``) wyciszy błędy poczty + wszystkim uczelniom obsługiwanym przez ten proces. + """ + filters: list[tuple[type[BaseException], str]] = [] + + if ignoruj_bledy_uwierzytelniania_smtp: + filters.append((smtplib.SMTPAuthenticationError, "ignored")) + + return filters diff --git a/src/django_bpp/settings/base.py b/src/django_bpp/settings/base.py index 67d4589ca..2b2ef4d33 100644 --- a/src/django_bpp/settings/base.py +++ b/src/django_bpp/settings/base.py @@ -15,6 +15,7 @@ from bpp.util import slugify_function from django_bpp.channels_prefix import get_channels_prefix +from django_bpp.rollbar_filters import zbuduj_exception_level_filters from django_bpp.version import VERSION logger = logging.getLogger(__name__) @@ -160,6 +161,12 @@ def int_or_none(v): ROLLBAR_ACCESS_TOKEN=(str, None), # Publiczny token klienta (post_client_item) do frontendowego Rollbara. ROLLBAR_CLIENT_ACCESS_TOKEN=(str, ""), + # Wycisza w Rollbarze smtplib.SMTPAuthenticationError (i tylko ją). + # Ustawiane PER INSTALACJA, domyślnie WYŁĄCZONE — szczegóły i uzasadnienie + # zakresu w django_bpp.rollbar_filters. Włączać wyłącznie tam, gdzie + # administratorzy poczty klienta mają znaną, zgłoszoną awarię po swojej + # stronie, której nie naprawimy kodem. + DJANGO_BPP_ROLLBAR_IGNORE_SMTP_AUTH_ERRORS=(bool, False), # # Prometheus # @@ -1764,6 +1771,11 @@ def iter_namespace(ns_pkg): re.compile(r"/favicon\.ico"), re.compile(r".*\{\{\s*clickURL\s*\}\}$"), ), + "exception_level_filters": zbuduj_exception_level_filters( + ignoruj_bledy_uwierzytelniania_smtp=env( + "DJANGO_BPP_ROLLBAR_IGNORE_SMTP_AUTH_ERRORS" + ), + ), } # Publiczny token klienta (post_client_item) do frontendowego Rollbara. diff --git a/src/django_bpp/tests/test_rollbar_filters.py b/src/django_bpp/tests/test_rollbar_filters.py new file mode 100644 index 000000000..dcd1c90e4 --- /dev/null +++ b/src/django_bpp/tests/test_rollbar_filters.py @@ -0,0 +1,97 @@ +"""Wyciszanie błędów uwierzytelniania SMTP w Rollbarze — opt-in per instalacja. + +Kontekst (Rollbar #379 i #1554 — bpp.umlub.pl): serwer SMTP uczelni odrzuca +poświadczenia (``535 5.7.3 Authentication unsuccessful``). To awaria po stronie +administratorów poczty klienta, której nie naprawimy kodem. Wyciszamy — ale +WYŁĄCZNIE na tej instalacji i WYŁĄCZNIE ten jeden wyjątek. +""" + +import smtplib +import sys + +import rollbar + +from django_bpp.rollbar_filters import zbuduj_exception_level_filters + + +def _wyslane_payloady(monkeypatch, filters, wyjatek): + """Przepuszcza ``wyjatek`` przez PRAWDZIWĄ ścieżkę raportowania pyrollbara. + + Świadomie NIE odpytujemy ``rollbar._is_ignored`` — ta funkcja nie jest + w pyrollbar 1.4.0 nigdzie wywoływana (jedyne wystąpienie to jej własna + definicja). Realne tłumienie idzie przez ``_filtered_level`` → + ``events.on_exception_info(level=...)`` → ``filters.basic.filter_by_level``. + Test odpytujący martwy kod dawałby fałszywą pewność dokładnie tam, gdzie + ma jej dostarczać. + """ + wyslane = [] + # `report_exc_info` nic nie robi, dopóki pyrollbar nie przejdzie `init()` + # — a w testach nie przechodzi (brak tokena). Inicjujemy więc jawnie, + # tokenem-atrapą, i przechwytujemy wysyłkę zamiast jej blokować. + monkeypatch.setattr(rollbar, "_initialized", True) + monkeypatch.setattr(rollbar, "send_payload", lambda p, t: wyslane.append(p)) + monkeypatch.setitem(rollbar.SETTINGS, "access_token", "atrapa") + monkeypatch.setitem(rollbar.SETTINGS, "exception_level_filters", filters) + monkeypatch.setitem(rollbar.SETTINGS, "handler", "blocking") + monkeypatch.setitem(rollbar.SETTINGS, "enabled", True) + + try: + raise wyjatek + except BaseException: + rollbar.report_exc_info(sys.exc_info()) + + return wyslane + + +def test_domyslnie_bledy_smtp_sa_raportowane(monkeypatch): + wyslane = _wyslane_payloady( + monkeypatch, + zbuduj_exception_level_filters(), + smtplib.SMTPAuthenticationError(535, b"nope"), + ) + + assert len(wyslane) == 1 + + +def test_wlaczona_flaga_naprawde_nie_wysyla_bledu_uwierzytelniania(monkeypatch): + """Realna ścieżka wysyłki, nie sam kształt listy filtrów.""" + wyslane = _wyslane_payloady( + monkeypatch, + zbuduj_exception_level_filters(ignoruj_bledy_uwierzytelniania_smtp=True), + smtplib.SMTPAuthenticationError(535, b"nope"), + ) + + assert wyslane == [] + + +def test_flaga_nie_wycisza_bledow_poczty_wynikajacych_z_NASZYCH_danych(monkeypatch): + """``SMTPRecipientsRefused`` to zły adres w naszej bazie — chcemy wiedzieć. + + Zakres wyciszenia jest celowo wąski: awaria po stronie klienta to problem + uwierzytelniania, a nie każdy błąd poczty. Odrzucony odbiorca i odrzucony + nadawca wskazują na nasze dane/konfigurację i muszą być widoczne także na + instalacji z wyciszeniem. + """ + filters = zbuduj_exception_level_filters(ignoruj_bledy_uwierzytelniania_smtp=True) + + for wyjatek in ( + smtplib.SMTPRecipientsRefused({"zly@adres": (550, b"no such user")}), + smtplib.SMTPSenderRefused(553, b"bad sender", "bpp@example.com"), + ): + assert len(_wyslane_payloady(monkeypatch, filters, wyjatek)) == 1 + + +def test_flaga_nie_wycisza_niczego_spoza_poczty(monkeypatch): + filters = zbuduj_exception_level_filters(ignoruj_bledy_uwierzytelniania_smtp=True) + + assert len(_wyslane_payloady(monkeypatch, filters, ValueError("cokolwiek"))) == 1 + + +def test_ustawienia_faktycznie_wpinaja_filtry_do_rollbara(): + """Sama funkcja nic nie da, jeśli nikt jej nie zawoła w ``settings``.""" + from django.conf import settings + + assert "exception_level_filters" in settings.ROLLBAR + # W testach flaga jest wyłączona → żadnych wyciszeń. To jest też asercja + # bezpieczeństwa: domyślna instalacja NIE gubi błędów. + assert settings.ROLLBAR["exception_level_filters"] == []