diff --git a/src/bpp/newsfragments/+zglos-podpowiedz-jednostki-403.bugfix.rst b/src/bpp/newsfragments/+zglos-podpowiedz-jednostki-403.bugfix.rst new file mode 100644 index 000000000..c36732f30 --- /dev/null +++ b/src/bpp/newsfragments/+zglos-podpowiedz-jednostki-403.bugfix.rst @@ -0,0 +1,7 @@ +Formularz „Zgłoś publikację" znów podpowiada jednostkę i dyscyplinę autora +zalogowanym użytkownikom bez uprawnień redaktorskich. Endpoint, z którego +korzysta ta podpowiedź — tylko czytający dane, niczego nie zapisujący — +trafił przez pomyłkę pod bramkę uprawnień redaktorskich, więc taki +użytkownik dostawał w tle błąd i tracił podpowiedź (po cichu, bo to +zapytanie w tle). Dla niezalogowanych podpowiedź nadal nie działa — tak jak +wcześniej. diff --git a/src/bpp/tests/test_authz_views.py b/src/bpp/tests/test_authz_views.py index 92a1d2b04..a08dfe8e5 100644 --- a/src/bpp/tests/test_authz_views.py +++ b/src/bpp/tests/test_authz_views.py @@ -64,16 +64,27 @@ def test_toz_anonim_przekierowuje(client): # --- API punktacji / habilitacji / jednostki ----------------------------- -API_ROUTES = [ +# Endpointy REDAKCYJNE: zalogowany bez uprawnień do wprowadzania danych → 403. +API_ROUTES_REDAKCYJNE = [ ("bpp:api_rok_habilitacji", {}), ("bpp:api_punktacja_zrodla", {"zrodlo_id": 1, "rok": 2020}), ("bpp:api_upload_punktacja_zrodla", {"zrodlo_id": 1, "rok": 2020}), +] + +# Wszystkie endpointy API wymagające ZALOGOWANIA (anonim → 302 na login). +# `api_ostatnia_jednostka_i_dyscyplina` jest tutaj, ale ŚWIADOMIE nie ma go na +# liście redakcyjnej wyżej: to widok tylko-do-odczytu, konsumowany przez +# `autorform_dependant.js` w PUBLICZNYM formularzu `zglos_publikacje`, więc +# zwykły zalogowany użytkownik musi dostać 200, a nie 403. Pilnuje tego +# `test_ostatnia_jednostka_dostepna_bez_uprawnien_redaktorskich` +# w `src/bpp/tests/test_views/test_api.py`. +API_ROUTES = API_ROUTES_REDAKCYJNE + [ ("bpp:api_ostatnia_jednostka_i_dyscyplina", {}), ] @pytest.mark.django_db -@pytest.mark.parametrize("name,kwargs", API_ROUTES) +@pytest.mark.parametrize("name,kwargs", API_ROUTES_REDAKCYJNE) def test_api_zwykly_user_403(client, zwykly_user, name, kwargs): client.force_login(zwykly_user) url = reverse(name, kwargs=kwargs) diff --git a/src/bpp/tests/test_views/test_api.py b/src/bpp/tests/test_views/test_api.py index 66646c897..388f56a27 100644 --- a/src/bpp/tests/test_views/test_api.py +++ b/src/bpp/tests/test_views/test_api.py @@ -3,9 +3,11 @@ import pytest from django.urls import reverse +from model_bakery import baker from bpp.models import Autor_Dyscyplina, Typ_Odpowiedzialnosci from bpp.models.zrodlo import Punktacja_Zrodla +from bpp.permissions import moze_wprowadzac_dane from bpp.tests.util import CURRENT_YEAR, any_autor, any_habilitacja, any_zrodlo from bpp.views.api import ( OstatniaJednostkaIDyscyplinaView, @@ -420,6 +422,67 @@ def test_api_endpoints_require_login(client, url_name, url_kwargs, post_data): ) +@pytest.mark.django_db +def test_ostatnia_jednostka_dostepna_bez_uprawnien_redaktorskich( + client, autor, jednostka +): + """Podpowiadanie jednostki działa dla ZALOGOWANEGO usera bez uprawnień + redaktorskich. + + Regresja (Rollbar #1532 i ~19 bliźniaczych itemów): endpoint dostał + ``WprowadzanieDanychRequiredMixin``, choć niczego nie mutuje — tylko czyta. + Konsumuje go ``autorform_dependant.js`` ładowany do PUBLICZNEGO formularza + ``zglos_publikacje``, więc każdy zgłaszający bez roli redaktora dostawał + 403 i tracił podpowiedź jednostki (po cichu — to AJAX). + """ + jednostka.dodaj_autora(autor) + + user = baker.make("bpp.BppUser", is_staff=False, is_superuser=False) + assert not moze_wprowadzac_dane(user) + client.force_login(user) + + url = reverse("bpp:api_ostatnia_jednostka_i_dyscyplina") + response = client.post(url, data={"autor_id": autor.pk, "rok": CURRENT_YEAR}) + + assert response.status_code == 200 + assert json.loads(response.content)["jednostka_id"] == jednostka.pk + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "url_name,url_kwargs,post_data", + [ + ("bpp:api_rok_habilitacji", {}, {"autor_pk": 1}), + ("bpp:api_punktacja_zrodla", {"zrodlo_id": 1, "rok": CURRENT_YEAR}, {}), + ( + "bpp:api_upload_punktacja_zrodla", + {"zrodlo_id": 1, "rok": CURRENT_YEAR}, + {"impact_factor": "50.0"}, + ), + ], +) +def test_pozostale_api_nadal_wymagaja_uprawnien_redaktorskich( + client, url_name, url_kwargs, post_data +): + """Lustro poprzedniego testu: poluzowanie dotyczy JEDNEGO widoku. + + Bez tego nic nie broni przed przyszłym „skoro tamten odblokowaliśmy, to + odblokujmy wszystkie" — a te trzy albo mutują dane + (``UploadPunktacjaZrodlaView``), albo wystawiają dane redakcyjne + konsumowane wyłącznie przez JS admina. + """ + user = baker.make("bpp.BppUser", is_staff=False, is_superuser=False) + assert not moze_wprowadzac_dane(user) + client.force_login(user) + + response = client.post(reverse(url_name, kwargs=url_kwargs), data=post_data) + + assert response.status_code == 403, ( + f"{url_name} przepuszcza zalogowanego bez uprawnień redaktorskich " + f"(status={response.status_code})" + ) + + @pytest.mark.django_db def test_upload_punktacja_zrodla_anon_does_not_write(client): """Najtwardszy regression test: anonim NIE może utworzyć Punktacja_Zrodla.""" diff --git a/src/bpp/views/api/__init__.py b/src/bpp/views/api/__init__.py index c9af13b48..8aa10f404 100644 --- a/src/bpp/views/api/__init__.py +++ b/src/bpp/views/api/__init__.py @@ -1,5 +1,6 @@ from decimal import Decimal, InvalidOperation +from django.contrib.auth.mixins import LoginRequiredMixin from django.db import models, transaction from django.http import JsonResponse from django.http.response import HttpResponseNotFound @@ -154,9 +155,22 @@ def ostatnia_dyscyplina(request, a, rok): return ad.dyscyplina_naukowa or ad.subdyscyplina_naukowa -class OstatniaJednostkaIDyscyplinaView(WprowadzanieDanychRequiredMixin, View): +class OstatniaJednostkaIDyscyplinaView(LoginRequiredMixin, View): """Zwraca jako JSON ostatnią jednostkę danego autora oraz ewentualnie jego dyscyplinę naukową, w sytuacji gdy jest ona jedna i określona na dany rok. + + ŚWIADOMIE ``LoginRequiredMixin``, a NIE + ``WprowadzanieDanychRequiredMixin``: widok niczego nie mutuje — czyta + ``Autor``/``Autor_Dyscyplina`` i zwraca JSON. Konsumuje go + ``autorform_dependant.js``, ładowany także do PUBLICZNEGO formularza + ``zglos_publikacje`` (patrz ``zglos_publikacje.forms``), więc bramka + redaktorska odcinała zwykłych zgłaszających od podpowiedzi jednostki + i dyscypliny — po cichu, bo to AJAX. + + Anonim NADAL dostaje 302 na login (kontrakt ``LoginRequiredMixin``), więc + dla niezalogowanych zgłaszających podpowiedź nie działa — tak samo jak + przed ``e892142ff``. Poluzowanie tego to osobna decyzja: endpoint jest + oraklem istnienia autora dla całej przestrzeni PK i nie ma rate-limitu. """ def post(self, request, *args, **kw):