Skip to content

fix(zglos): podpowiedź jednostki znów działa dla nie-redaktorów (403→200)#673

Open
mpasternak wants to merge 3 commits into
devfrom
fix/zglos-podpowiedz-jednostki-403
Open

fix(zglos): podpowiedź jednostki znów działa dla nie-redaktorów (403→200)#673
mpasternak wants to merge 3 commits into
devfrom
fix/zglos-podpowiedz-jednostki-403

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Problem

Endpoint /bpp/api/ostatnia-jednostka-i-dyscyplina/ dostał w e892142ff
(„fix(security): wymagaj uprawnień redaktorskich dla operacji mutujących")
mixin WprowadzanieDanychRequiredMixinchoć niczego nie mutuje: czyta
Autor/Autor_Dyscyplina i zwraca JSON.

Ten endpoint konsumuje autorform_dependant.js, ładowany przez
zglos_publikacje/forms.py:487 do publicznego formularza „Zgłoś
publikację". Efekt: każdy zgłaszający bez roli redaktora dostawał 403 i tracił
podpowiedź jednostki oraz dyscypliny — po cichu, bo to zapytanie AJAX.

W Rollbarze objawiało się to ~19 osobnymi itemami PermissionDenied: Brak uprawnień do wprowadzania danych. (po jednym na wystąpienie), m.in.
#1532,
#1495,
#1486
to była mniej więcej połowa świeżego strumienia błędów produkcyjnych.

Rozwiązanie

Powrót do LoginRequiredMixin (stan sprzed e892142ff) + komentarz w
docstringu, żeby następny audyt bezpieczeństwa nie zaklasyfikował tego widoku
ponownie jako mutującego.

Anonim nadal dostaje redirect na login — pokrywa to istniejący
test_api_endpoints_require_login.

Testy

Nowy test_ostatnia_jednostka_dostepna_bez_uprawnien_redaktorskich — TDD:
najpierw padał na assert 403 == 200, po zmianie przechodzi.

  • src/bpp/tests/test_views/test_api.py — 31 passed
  • src/bpp/tests/test_permissions.py + src/zglos_publikacje — 190 passed
    (z Playwrightem)

🤖 Generated with Claude Code

https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH

mpasternak and others added 2 commits July 24, 2026 23:37
…200)

Endpoint /bpp/api/ostatnia-jednostka-i-dyscyplina/ dostał w e892142
bramkę WprowadzanieDanychRequiredMixin, choć niczego nie mutuje — czyta
Autor/Autor_Dyscyplina i zwraca JSON. Konsumuje go autorform_dependant.js,
ładowany także do PUBLICZNEGO formularza zglos_publikacje, więc każdy
zgłaszający bez roli redaktora dostawał 403 i tracił podpowiedź jednostki
oraz dyscypliny — po cichu, bo to AJAX.

Wracamy do LoginRequiredMixin (stan sprzed e892142): anonim nadal
dostaje redirect na login, co pokrywa istniejący test
test_api_endpoints_require_login.

W Rollbarze objawiało się to ~19 osobnymi itemami PermissionDenied
(m.in. #1532, #1495, #1486) — po jednym na wystąpienie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH
- Newsfragment obiecywał, że formularz "znów podpowiada" — dla ANONIMA to
  nieprawda: LoginRequiredMixin nadal daje mu 302, tak samo jak przed
  e892142. Formularz zgłoszeń przepuszcza niezalogowanych, więc to realna
  grupa. Doprecyzowane w newsfragmencie i w docstringu widoku, wraz z
  powodem, dla którego nie luzujemy tego teraz (endpoint jest oraklem
  istnienia autora dla całej przestrzeni PK, bez rate-limitu).

- Nowy test-lustro: pozostałe trzy widoki API (rok habilitacji, punktacja
  źródła, upload punktacji) NADAL dają 403 zalogowanemu bez uprawnień
  redaktorskich. Bez tego nic nie broni przed przyszłym "skoro tamten
  odblokowaliśmy, odblokujmy wszystkie".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH
@mpasternak

Copy link
Copy Markdown
Member Author

Poprawki po self-review

Naniesione w 4b7278650:

1. Newsfragment obiecywał za dużo. „Formularz znów podpowiada" — dla
anonima to nieprawda: LoginRequiredMixin nadal daje mu 302, dokładnie tak
jak przed e892142ff. A Uczelnia.wymagaj_logowania_zglos_publikacje ma
default=False i wizard przepuszcza niezalogowanych (stąd ALTCHA), więc to
realna grupa użytkowników. Doprecyzowane w newsfragmencie i w docstringu widoku
— razem z powodem, dla którego nie luzujemy tego teraz: endpoint jest
oraklem istnienia autora dla całej przestrzeni PK i nie ma rate-limitu.

2. Test-lustro. Nowy test_pozostale_api_nadal_wymagaja_uprawnien_redaktorskich
sprawdza, że api_rok_habilitacji, api_punktacja_zrodla i
api_upload_punktacja_zrodla nadal dają 403 zalogowanemu bez uprawnień.
Bez tego nic nie broni przed przyszłym „skoro tamten odblokowaliśmy, odblokujmy
wszystkie". 34 passed.

Znaleziska poza zakresem tego PR-a (do osobnego wątku)

A. Podpowiedź może wstrzyknąć jednostkę, której formularz nie przyjmie.
Zweryfikowane: Zgloszenie_Publikacji_AutorForm.jednostka ma
queryset=Jednostka.objects.publiczne() i clean_jednostka() odrzuca jednostki
z przyjmuje_afiliacje() == False. autorform_dependant.js dokleja
<option selected> z pominięciem queryseta. Gdy autor nie ma
aktualna_jednostka, ostatnia_jednostka() zwraca uczelnia.obca_jednostka
a ta ma skupia_pracownikow=False, więc formularz ją odrzuci.

Skutek jest łagodny (czytelny komunikat „Do jednostki … nie można afiliować
autora"), a bug jest starszy niż e892142ff — był tylko zamaskowany przez 403.
Nie naprawiam tutaj, bo filtr publiczne() w ostatnia_jednostka() zepsułby
admina: autorform_dependant.js jest ładowany także przez bpp/admin/core.py,
gdzie redaktor legalnie przypisuje do jednostek niepublicznych. Potrzebne
rozróżnienie kontekstu — osobny PR.

B. Brak scopingu po uczelni (IDOR wielotenantowy). Autor.objects.get(pk=...)
nie jest zawężany przez Uczelnia.objects.get_for_request(), więc zalogowany na
hoście uczelni A może odpytać autora uczelni B. To świadomie odroczone już
w e892142ff („Poza zakresem (świadomie): scoping bieżącej uczelni przy
pobraniu po globalnym PK — osobny PR"). Ten PR nie tworzy luki, ale poszerza
krąg odbiorców z redaktorów na wszystkich zalogowanych — odnotowuję świadomie.

C. Dlaczego CI tego nie złapało. Playwrightowy test podpowiedzi
(test_zglos_publikacje.py, wait_for_discipline_populated) jedzie na fixture
admin_page, czyli jako superuser — a superuser przechodzi
moze_wprowadzac_dane. E2E publicznego formularza nigdy nie dotknął ścieżki
zwykłego użytkownika. Wariant tego testu na nie-redaktorze byłby właściwą
barierą; nie dokładam go tutaj, bo nowy test jednostkowy pokrywa regresję,
a E2E znacząco wydłuża suitę.

… konflikt)

CI wywaliło test_api_zwykly_user_403[bpp:api_ostatnia_jednostka_i_dyscyplina]
— istniejący test w src/bpp/tests/test_authz_views.py asertował 403 dla tego
endpointu, a to jest dokładnie zachowanie, które ten PR zmienia. Przegapiłem
ten plik, bo lokalnie uruchamiałem testy celowane (test_api.py,
test_permissions.py, zglos_publikacje), a nie całe src/bpp/tests.

API_ROUTES było wspólną listą dla dwóch testów o różnych kontraktach.
Rozdzielone:

- API_ROUTES_REDAKCYJNE — zalogowany bez uprawnień → 403 (trzy widoki
  mutujące/redakcyjne),
- API_ROUTES = powyższe + ostatnia_jednostka — anonim → 302 na login.
  Ten kontrakt się NIE zmienia i nadal jest pilnowany.

Komentarz przy liście wyjaśnia, dlaczego ostatnia_jednostka jest w drugiej,
a nie w pierwszej, i wskazuje test pilnujący jej dostępności (200).

Zweryfikowane pełnym przebiegiem src/bpp/tests: 2850 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant