Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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ć)
#
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions src/django_bpp/rollbar_filters.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions src/django_bpp/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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.
Expand Down
97 changes: 97 additions & 0 deletions src/django_bpp/tests/test_rollbar_filters.py
Original file line number Diff line number Diff line change
@@ -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"] == []