From 9859c3b8ef030f67fd005e0d06d5332c0a963e84 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez Date: Thu, 28 Nov 2024 11:53:40 +0100 Subject: [PATCH] settings: add LoginRequiredMiddleware --- config/settings/base.py | 1 + config/urls.py | 25 ++++++++++++++--- itou/api/applicants_api/views.py | 3 ++- itou/api/c4_api/views.py | 3 ++- itou/api/data_inclusion_api/views.py | 3 ++- itou/api/employee_record_api/viewsets.py | 3 ++- itou/api/geiq/views.py | 3 ++- itou/api/redoc_views.py | 4 ++- itou/api/siae_api/viewsets.py | 3 ++- itou/api/token_auth/views.py | 3 ++- itou/api/urls.py | 3 ++- itou/openid_connect/france_connect/views.py | 4 +++ .../openid_connect/inclusion_connect/views.py | 5 ++++ itou/openid_connect/pe_connect/views.py | 5 ++++ itou/openid_connect/pro_connect/views.py | 5 ++++ itou/status/views.py | 2 ++ itou/utils/auth.py | 8 ++++++ itou/utils/redirect_legacy_views.py | 2 ++ itou/www/announcements/views.py | 3 ++- itou/www/api/urls.py | 3 ++- itou/www/autocomplete/views.py | 5 +++- itou/www/companies_views/views.py | 9 ++++--- itou/www/error.py | 2 ++ itou/www/home/views.py | 2 ++ itou/www/invitations_views/views.py | 3 ++- itou/www/login/views.py | 3 ++- itou/www/prescribers_views/views.py | 3 ++- itou/www/rdv_insertion/views.py | 2 ++ itou/www/releases/views.py | 2 ++ itou/www/search/views.py | 7 ++++- itou/www/siae_evaluations_views/views.py | 3 ++- itou/www/signup/urls.py | 3 ++- itou/www/signup/views.py | 27 ++++++++++++++----- itou/www/stats/views.py | 3 ++- tests/www/apply/test_submit.py | 12 ++++----- tests/www/signup/test_prescriber.py | 7 ++++- 36 files changed, 145 insertions(+), 39 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index e6ded1f8a07..8810e29e08f 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -135,6 +135,7 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.LoginRequiredMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", # Third party diff --git a/config/urls.py b/config/urls.py index 0ff8378318c..4bf41685d38 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib import admin +from django.contrib.auth.decorators import login_not_required from django.urls import include, path, re_path, register_converter from django.views.generic import TemplateView @@ -91,10 +92,26 @@ re_path(r"^webhooks/anymail/", include("anymail.urls")), path("welcoming_tour/", include("itou.www.welcoming_tour.urls")), # Static pages. - path("accessibility/", TemplateView.as_view(template_name="static/accessibility.html"), name="accessibility"), - path("legal/notice/", TemplateView.as_view(template_name="static/legal/notice.html"), name="legal-notice"), - path("legal/privacy/", TemplateView.as_view(template_name="static/legal/privacy.html"), name="legal-privacy"), - path("legal/terms/", TemplateView.as_view(template_name="static/legal/terms.html"), name="legal-terms"), + path( + "accessibility/", + login_not_required(TemplateView.as_view(template_name="static/accessibility.html")), + name="accessibility", + ), + path( + "legal/notice/", + login_not_required(TemplateView.as_view(template_name="static/legal/notice.html")), + name="legal-notice", + ), + path( + "legal/privacy/", + login_not_required(TemplateView.as_view(template_name="static/legal/privacy.html")), + name="legal-privacy", + ), + path( + "legal/terms/", + login_not_required(TemplateView.as_view(template_name="static/legal/terms.html")), + name="legal-terms", + ), path("", include("itou.www.security.urls")), path("gps/", include("itou.www.gps.urls")), path("rdvi/", include("itou.www.rdv_insertion.urls")), diff --git a/itou/api/applicants_api/views.py b/itou/api/applicants_api/views.py index b6affaa105e..6cc22ee6756 100644 --- a/itou/api/applicants_api/views.py +++ b/itou/api/applicants_api/views.py @@ -8,12 +8,13 @@ from itou.job_applications.models import JobApplication from itou.users.enums import UserKind from itou.users.models import User +from itou.utils.auth import LoginNotRequiredMixin from .perms import ApplicantsAPIPermission from .serializers import APIParametersSerializer, ApplicantSerializer -class ApplicantsView(generics.ListAPIView): +class ApplicantsView(LoginNotRequiredMixin, generics.ListAPIView): authentication_classes = ( authentication.TokenAuthentication, authentication.SessionAuthentication, diff --git a/itou/api/c4_api/views.py b/itou/api/c4_api/views.py index ba3c8626cde..ae2be20b0e3 100644 --- a/itou/api/c4_api/views.py +++ b/itou/api/c4_api/views.py @@ -6,6 +6,7 @@ from itou.api.c4_api.serializers import C4CompanySerializer from itou.companies.enums import COMPANY_KIND_RESERVED from itou.companies.models import Company, CompanyMembership +from itou.utils.auth import LoginNotRequiredMixin class C4APIUser(AnonymousUser): @@ -27,7 +28,7 @@ def authenticate_credentials(self, key): return C4APIUser(), None -class C4CompanyView(generics.ListAPIView): +class C4CompanyView(LoginNotRequiredMixin, generics.ListAPIView): """API pour le Marché de l'inclusion""" authentication_classes = [C4Authentication] diff --git a/itou/api/data_inclusion_api/views.py b/itou/api/data_inclusion_api/views.py index 7d7f0da5564..5e144007c24 100644 --- a/itou/api/data_inclusion_api/views.py +++ b/itou/api/data_inclusion_api/views.py @@ -5,6 +5,7 @@ from itou.api.data_inclusion_api import enums, serializers from itou.companies.models import Company from itou.prescribers.models import PrescriberOrganization +from itou.utils.auth import LoginNotRequiredMixin @extend_schema( @@ -18,7 +19,7 @@ many=True, ) ) -class DataInclusionStructureView(generics.ListAPIView): +class DataInclusionStructureView(LoginNotRequiredMixin, generics.ListAPIView): """ # API au format data.inclusion diff --git a/itou/api/employee_record_api/viewsets.py b/itou/api/employee_record_api/viewsets.py index 84e9b601703..aaef9d99a88 100644 --- a/itou/api/employee_record_api/viewsets.py +++ b/itou/api/employee_record_api/viewsets.py @@ -8,6 +8,7 @@ from itou.api import AUTH_TOKEN_EXPLANATION_TEXT from itou.employee_record.models import EmployeeRecord, EmployeeRecordUpdateNotification, Status +from itou.utils.auth import LoginNotRequiredMixin from .perms import EmployeeRecordAPIPermission from .serializers import EmployeeRecordAPISerializer, EmployeeRecordUpdateNotificationAPISerializer @@ -22,7 +23,7 @@ class EmployeeRecordRateThrottle(UserRateThrottle): rate = "60/min" -class AbstractEmployeeRecordViewSet(viewsets.ReadOnlyModelViewSet): +class AbstractEmployeeRecordViewSet(LoginNotRequiredMixin, viewsets.ReadOnlyModelViewSet): throttle_classes = [EmployeeRecordRateThrottle] # Possible authentication frameworks: diff --git a/itou/api/geiq/views.py b/itou/api/geiq/views.py index c9da50a1e12..54b991d9909 100644 --- a/itou/api/geiq/views.py +++ b/itou/api/geiq/views.py @@ -10,6 +10,7 @@ from itou.companies.models import Company from itou.job_applications.enums import JobApplicationState, Prequalification, ProfessionalSituationExperience from itou.job_applications.models import JobApplication, PriorAction +from itou.utils.auth import LoginNotRequiredMixin from itou.utils.validators import validate_siren from .serializers import GeiqJobApplicationSerializer @@ -41,7 +42,7 @@ class InvalidSirenError(exceptions.APIException): status_code = status.HTTP_400_BAD_REQUEST -class GeiqJobApplicationListView(generics.ListAPIView): +class GeiqJobApplicationListView(LoginNotRequiredMixin, generics.ListAPIView): authentication_classes = ( GeiqApiAuthentication, authentication.SessionAuthentication, diff --git a/itou/api/redoc_views.py b/itou/api/redoc_views.py index dc6707d791b..e3464885b79 100644 --- a/itou/api/redoc_views.py +++ b/itou/api/redoc_views.py @@ -1,8 +1,10 @@ from django.templatetags.static import static from drf_spectacular.views import SpectacularRedocView +from itou.utils.auth import LoginNotRequiredMixin -class ItouSpectacularRedocView(SpectacularRedocView): + +class ItouSpectacularRedocView(LoginNotRequiredMixin, SpectacularRedocView): @staticmethod def _redoc_standalone(): return static("vendor/redoc/redoc.standalone.js") diff --git a/itou/api/siae_api/viewsets.py b/itou/api/siae_api/viewsets.py index 43a8f431bf1..b4817f309c3 100644 --- a/itou/api/siae_api/viewsets.py +++ b/itou/api/siae_api/viewsets.py @@ -14,6 +14,7 @@ from itou.cities.models import City from itou.common_apps.address.departments import DEPARTMENTS from itou.companies.models import Company, JobDescription +from itou.utils.auth import LoginNotRequiredMixin from .serializers import SiaeSerializer @@ -119,7 +120,7 @@ class RestrictedUserRateThrottle(UserRateThrottle): rate = "12/minute" -class SiaeViewSet(viewsets.ReadOnlyModelViewSet): +class SiaeViewSet(LoginNotRequiredMixin, viewsets.ReadOnlyModelViewSet): """ # Liste des SIAE diff --git a/itou/api/token_auth/views.py b/itou/api/token_auth/views.py index 5ef3cc81671..166c29831cb 100644 --- a/itou/api/token_auth/views.py +++ b/itou/api/token_auth/views.py @@ -6,6 +6,7 @@ from rest_framework.response import Response from itou.api import AUTH_TOKEN_EXPLANATION_TEXT +from itou.utils.auth import LoginNotRequiredMixin logger = logging.getLogger(__name__) @@ -14,7 +15,7 @@ TOKEN_ID_STR = "__token__" -class ObtainAuthToken(drf_authtoken_views.ObtainAuthToken): +class ObtainAuthToken(LoginNotRequiredMixin, drf_authtoken_views.ObtainAuthToken): def post(self, request, *args, **kwargs): if request.data.get("username") == TOKEN_ID_STR: password = request.data.get("password") diff --git a/itou/api/urls.py b/itou/api/urls.py index d8fd9682f6b..ad2d469338c 100644 --- a/itou/api/urls.py +++ b/itou/api/urls.py @@ -1,3 +1,4 @@ +from django.contrib.auth.decorators import login_not_required from django.urls import include, path from drf_spectacular.views import SpectacularAPIView from rest_framework import routers @@ -41,7 +42,7 @@ # OAS 3 YAML schema (downloadable) path( "oas3/", - SpectacularAPIView.as_view(), + login_not_required(SpectacularAPIView.as_view()), name="openapi_schema", ), path( diff --git a/itou/openid_connect/france_connect/views.py b/itou/openid_connect/france_connect/views.py index 5793e80364c..975c7d301c5 100644 --- a/itou/openid_connect/france_connect/views.py +++ b/itou/openid_connect/france_connect/views.py @@ -5,6 +5,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import login +from django.contrib.auth.decorators import login_not_required from django.http import HttpResponseRedirect, JsonResponse from django.urls import reverse from django.utils import crypto @@ -34,6 +35,7 @@ def _redirect_to_job_seeker_login_on_error(error_msg, request=None, extra_tags=" return HttpResponseRedirect(reverse("login:job_seeker")) +@login_not_required def france_connect_authorize(request): # The redirect_uri should be defined in the FC settings to be allowed # NB: the integration platform allows "http://127.0.0.1:8000/franceconnect/callback" @@ -52,6 +54,7 @@ def france_connect_authorize(request): return HttpResponseRedirect(f"{url}?{urlencode(data)}") +@login_not_required def france_connect_callback(request): code = request.GET.get("code") if code is None: @@ -160,6 +163,7 @@ def france_connect_callback(request): return HttpResponseRedirect(next_url) +@login_not_required def france_connect_logout(request): # The user can be authentified on FC w/o a session on itou. # https://partenaires.franceconnect.gouv.fr/fcp/fournisseur-service#sign_out diff --git a/itou/openid_connect/inclusion_connect/views.py b/itou/openid_connect/inclusion_connect/views.py index 885371f93f1..6e913448a1d 100644 --- a/itou/openid_connect/inclusion_connect/views.py +++ b/itou/openid_connect/inclusion_connect/views.py @@ -6,6 +6,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import login +from django.contrib.auth.decorators import login_not_required from django.http import HttpResponseRedirect from django.urls import reverse from django.utils import crypto @@ -120,6 +121,7 @@ def _add_user_kind_error_message(request, existing_user, new_user_kind): ) +@login_not_required def inclusion_connect_authorize(request): # Block access if ProConnect is enabled if settings.PRO_CONNECT_BASE_URL: @@ -166,6 +168,7 @@ def inclusion_connect_authorize(request): return HttpResponseRedirect(f"{base_url}?{urlencode(data)}") +@login_not_required def inclusion_connect_activate_account(request): params = request.GET.copy() email = params.get("user_email") @@ -228,6 +231,7 @@ def _get_user_info(request, access_token): return response.json(), None +@login_not_required def inclusion_connect_callback(request): # Block access if ProConnect is enabled if settings.PRO_CONNECT_BASE_URL: @@ -356,6 +360,7 @@ def inclusion_connect_callback(request): return HttpResponseRedirect(next_url) +@login_not_required def inclusion_connect_logout(request): token = request.GET.get("token") post_logout_redirect_url = request.GET.get("redirect_url", reverse("search:employers_home")) diff --git a/itou/openid_connect/pe_connect/views.py b/itou/openid_connect/pe_connect/views.py index 48e592f1430..dbf0d2970e6 100644 --- a/itou/openid_connect/pe_connect/views.py +++ b/itou/openid_connect/pe_connect/views.py @@ -5,6 +5,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import login +from django.contrib.auth.decorators import login_not_required from django.http import HttpResponseRedirect, JsonResponse from django.shortcuts import render from django.urls import reverse @@ -38,6 +39,7 @@ def _redirect_to_job_seeker_login_on_error(error_msg, request=None, extra_tags=" return HttpResponseRedirect(reverse("login:job_seeker")) +@login_not_required def pe_connect_authorize(request): # The redirect_uri should be defined in the PEAMU settings to be allowed # NB: the integration platform allows "http://127.0.0.1:8000/pe_connect/callback" @@ -56,6 +58,7 @@ def pe_connect_authorize(request): return HttpResponseRedirect(f"{url}?{urlencode(data)}") +@login_not_required def pe_connect_callback(request): code = request.GET.get("code") if code is None: @@ -183,10 +186,12 @@ def pe_connect_callback(request): return HttpResponseRedirect(next_url) +@login_not_required def pe_connect_no_email(request, template_name="account/peamu_no_email.html"): return render(request, template_name) +@login_not_required def pe_connect_logout(request): id_token = request.GET.get("id_token") diff --git a/itou/openid_connect/pro_connect/views.py b/itou/openid_connect/pro_connect/views.py index 3278c686cb3..fcedb8b3f9b 100644 --- a/itou/openid_connect/pro_connect/views.py +++ b/itou/openid_connect/pro_connect/views.py @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import login +from django.contrib.auth.decorators import login_not_required from django.http import HttpResponseRedirect from django.urls import reverse from django.utils import crypto @@ -109,6 +110,7 @@ def _add_user_kind_error_message(request, existing_user, new_user_kind): ) +@login_not_required def pro_connect_authorize(request): # Start a new session. user_kind = request.GET.get("user_kind") @@ -185,6 +187,7 @@ def _get_user_info(request, access_token): return decoded_id_token, None +@login_not_required def pro_connect_callback(request): code = request.GET.get("code") state = request.GET.get("state") @@ -307,6 +310,7 @@ def pro_connect_callback(request): return HttpResponseRedirect(next_url) +@login_not_required def pro_connect_logout(request): token = request.GET.get("token") post_logout_redirect_url = reverse("pro_connect:logout_callback") @@ -330,6 +334,7 @@ def pro_connect_logout(request): return HttpResponseRedirect(complete_url) +@login_not_required def pro_connect_logout_callback(request): state = request.GET.get("state") if state is None: diff --git a/itou/status/views.py b/itou/status/views.py index 782b20b5874..8022b4ac21b 100644 --- a/itou/status/views.py +++ b/itou/status/views.py @@ -1,8 +1,10 @@ +from django.contrib.auth.decorators import login_not_required from django.shortcuts import render from . import models, probes +@login_not_required def index(request): probes_classes = sorted(probes.get_probes_classes(), key=lambda p: p.name) probes_status_by_name = {ps.name: ps for ps in models.ProbeStatus.objects.all()} diff --git a/itou/utils/auth.py b/itou/utils/auth.py index 5458e57fb3e..189385755de 100644 --- a/itou/utils/auth.py +++ b/itou/utils/auth.py @@ -1,5 +1,6 @@ from functools import wraps +from django.contrib.auth.decorators import login_not_required from django.core.exceptions import PermissionDenied @@ -15,3 +16,10 @@ def _check_user_view_wrapper(request, *args, **kwargs): return wraps(view_func)(_check_user_view_wrapper) return decorator + + +class LoginNotRequiredMixin: + @classmethod + def as_view(cls, *args, **kwargs): + view = super().as_view(*args, **kwargs) + return login_not_required(view) diff --git a/itou/utils/redirect_legacy_views.py b/itou/utils/redirect_legacy_views.py index e592ca60409..9715520306f 100644 --- a/itou/utils/redirect_legacy_views.py +++ b/itou/utils/redirect_legacy_views.py @@ -1,5 +1,7 @@ +from django.contrib.auth.decorators import login_not_required from django.http import HttpResponsePermanentRedirect +@login_not_required def redirect_siaes_views(request, *args, **kwargs): return HttpResponsePermanentRedirect(request.get_full_path().replace("/siae", "/company", 1)) diff --git a/itou/www/announcements/views.py b/itou/www/announcements/views.py index f62a5838b54..ad16d5f8ffa 100644 --- a/itou/www/announcements/views.py +++ b/itou/www/announcements/views.py @@ -5,11 +5,12 @@ from itou.communications.models import AnnouncementCampaign, AnnouncementItem from itou.users.enums import UserKind +from itou.utils.auth import LoginNotRequiredMixin from itou.utils.pagination import pager from itou.utils.urls import get_safe_url -class NewsView(TemplateView): +class NewsView(LoginNotRequiredMixin, TemplateView): template_name = "announcements/news.html" def get_context_data(self): diff --git a/itou/www/api/urls.py b/itou/www/api/urls.py index 06441fcf8a6..d8e369348f0 100644 --- a/itou/www/api/urls.py +++ b/itou/www/api/urls.py @@ -1,3 +1,4 @@ +from django.contrib.auth.decorators import login_not_required from django.urls import path from django.views.generic import TemplateView @@ -6,5 +7,5 @@ urlpatterns = [ - path("", TemplateView.as_view(template_name="api/index.html"), name="index"), + path("", login_not_required(TemplateView.as_view(template_name="api/index.html")), name="index"), ] diff --git a/itou/www/autocomplete/views.py b/itou/www/autocomplete/views.py index 80afe4bc318..a27081e9c74 100644 --- a/itou/www/autocomplete/views.py +++ b/itou/www/autocomplete/views.py @@ -1,6 +1,6 @@ from datetime import datetime -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_not_required, login_required from django.db.models import F, Q, Value from django.db.models.functions import Least, Lower, NullIf, StrIndex from django.http import JsonResponse @@ -43,6 +43,7 @@ def autocomplete_name(qs, term, extra_ordering_by): ) +@login_not_required def cities_autocomplete(request): """ Returns JSON data compliant with Select2 @@ -66,6 +67,7 @@ def cities_autocomplete(request): return JsonResponse({"results": cities}, safe=False) +@login_not_required def jobs_autocomplete(request): """ Returns JSON data compliant with Select2 @@ -86,6 +88,7 @@ def jobs_autocomplete(request): return JsonResponse({"results": appellations}, safe=False) +@login_not_required def communes_autocomplete(request): """ Autocomplete endpoint for INSEE communes (ASP ref. files) diff --git a/itou/www/companies_views/views.py b/itou/www/companies_views/views.py index 555f797f69b..f183e71a2ac 100644 --- a/itou/www/companies_views/views.py +++ b/itou/www/companies_views/views.py @@ -3,7 +3,7 @@ from django.conf import settings from django.contrib import messages -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_not_required, login_required from django.core.cache import caches from django.core.exceptions import PermissionDenied from django.db.models import Count, Q @@ -23,6 +23,7 @@ from itou.utils import constants as global_constants from itou.utils.apis.data_inclusion import DataInclusionApiClient, DataInclusionApiException from itou.utils.apis.exceptions import GeocodingDataError +from itou.utils.auth import LoginNotRequiredMixin from itou.utils.pagination import pager from itou.utils.perms.company import get_current_company_or_404 from itou.utils.urls import add_url_params, get_absolute_url, get_safe_url @@ -124,7 +125,7 @@ def overview(request, template_name="companies/overview.html"): ### Job description views -class JobDescriptionCardView(ApplyForJobSeekerMixin, TemplateView): +class JobDescriptionCardView(LoginNotRequiredMixin, ApplyForJobSeekerMixin, TemplateView): template_name = "companies/job_description_card.html" def setup(self, request, job_description_id, *args, **kwargs): @@ -460,7 +461,7 @@ def select_financial_annex(request, template_name="companies/select_financial_an ### Company CRUD views -class CompanyCardView(ApplyForJobSeekerMixin, TemplateView): +class CompanyCardView(LoginNotRequiredMixin, ApplyForJobSeekerMixin, TemplateView): template_name = "companies/card.html" def setup(self, request, siae_id, *args, **kwargs): @@ -681,6 +682,7 @@ def update_admin_role(request, action, user_id, template_name="companies/update_ return render(request, template_name, context) +@login_not_required def hx_dora_services(request, code_insee, template_name="companies/hx_dora_services.html"): context = { "data_inclusion_services": get_data_inclusion_services(code_insee), @@ -689,6 +691,7 @@ def hx_dora_services(request, code_insee, template_name="companies/hx_dora_servi return render(request, template_name, context) +@login_not_required def dora_service_redirect(request, source: str, service_id: str) -> HttpResponseRedirect: client = DataInclusionApiClient( settings.API_DATA_INCLUSION_BASE_URL, diff --git a/itou/www/error.py b/itou/www/error.py index fa0de0bc4d5..fd1cebb4f65 100644 --- a/itou/www/error.py +++ b/itou/www/error.py @@ -1,5 +1,6 @@ import logging +from django.contrib.auth.decorators import login_not_required from django.http import HttpResponseServerError from django.template import TemplateDoesNotExist, engines, loader from django.views.decorators.csrf import requires_csrf_token @@ -7,6 +8,7 @@ @requires_csrf_token +@login_not_required def server_error(request, template_name=ERROR_500_TEMPLATE_NAME): try: template = loader.get_template(template_name) diff --git a/itou/www/home/views.py b/itou/www/home/views.py index cf2bc002d5a..c6f9406f194 100644 --- a/itou/www/home/views.py +++ b/itou/www/home/views.py @@ -1,7 +1,9 @@ +from django.contrib.auth.decorators import login_not_required from django.http import HttpResponseRedirect from django.urls import reverse +@login_not_required def home(request): if request.user.is_authenticated: return HttpResponseRedirect(reverse("dashboard:index")) diff --git a/itou/www/invitations_views/views.py b/itou/www/invitations_views/views.py index 516b30abea2..740c11b9109 100644 --- a/itou/www/invitations_views/views.py +++ b/itou/www/invitations_views/views.py @@ -3,7 +3,7 @@ from allauth.account.adapter import get_adapter from django.conf import settings from django.contrib import messages -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_not_required, login_required from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render, reverse from django.utils import formats, safestring @@ -72,6 +72,7 @@ def handle_invited_user_registration_with_inclusion_or_pro_connect(request, invi return render(request, "invitations_views/new_ic_user.html", context=context) +@login_not_required def new_user(request, invitation_type, invitation_id): if invitation_type not in [KIND_LABOR_INSPECTOR, KIND_PRESCRIBER, KIND_EMPLOYER]: messages.error(request, "Cette invitation n'est plus valide.") diff --git a/itou/www/login/views.py b/itou/www/login/views.py index b62d9621811..03c4ad7dd4d 100644 --- a/itou/www/login/views.py +++ b/itou/www/login/views.py @@ -10,11 +10,12 @@ from itou.openid_connect.inclusion_connect.enums import InclusionConnectChannel from itou.users.enums import MATOMO_ACCOUNT_TYPE, IdentityProvider, UserKind from itou.users.models import User +from itou.utils.auth import LoginNotRequiredMixin from itou.utils.urls import add_url_params, get_safe_url, get_url_param_value from itou.www.login.forms import ItouLoginForm -class ItouLoginView(LoginView): +class ItouLoginView(LoginNotRequiredMixin, LoginView): """ Generic authentication entry point. This view is used only in one case: diff --git a/itou/www/prescribers_views/views.py b/itou/www/prescribers_views/views.py index 7a7a2c3ce22..b712848e976 100644 --- a/itou/www/prescribers_views/views.py +++ b/itou/www/prescribers_views/views.py @@ -1,5 +1,5 @@ from django.contrib import messages -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_not_required, login_required from django.core.exceptions import PermissionDenied from django.db.models import Count, Q from django.http import HttpResponseRedirect @@ -16,6 +16,7 @@ from itou.www.prescribers_views.forms import EditPrescriberOrganizationForm +@login_not_required def card(request, org_id, template_name="prescribers/card.html"): prescriber_org = get_object_or_404(PrescriberOrganization, pk=org_id, is_authorized=True) back_url = get_safe_url(request, "back_url") diff --git a/itou/www/rdv_insertion/views.py b/itou/www/rdv_insertion/views.py index dd6ea353828..9377f34a4ae 100644 --- a/itou/www/rdv_insertion/views.py +++ b/itou/www/rdv_insertion/views.py @@ -5,6 +5,7 @@ import logging from django.conf import settings +from django.contrib.auth.decorators import login_not_required from django.db import transaction from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt @@ -32,6 +33,7 @@ class UnsupportedEvent(Warning): @require_POST @csrf_exempt +@login_not_required def webhook(request): try: # FIXME: RDV-I encodings should be consistent diff --git a/itou/www/releases/views.py b/itou/www/releases/views.py index 6faac7ebba9..7ed49ab86af 100644 --- a/itou/www/releases/views.py +++ b/itou/www/releases/views.py @@ -2,10 +2,12 @@ import markdown from django.conf import settings +from django.contrib.auth.decorators import login_not_required from django.shortcuts import render from django.utils.html import mark_safe +@login_not_required def releases(request, template_name="releases/list.html"): """ Render our CHANGELOG.md file in HTML diff --git a/itou/www/search/views.py b/itou/www/search/views.py index 3305c26590f..fe1c5156fd0 100644 --- a/itou/www/search/views.py +++ b/itou/www/search/views.py @@ -1,6 +1,7 @@ from collections import defaultdict, namedtuple from urllib.parse import urlencode +from django.contrib.auth.decorators import login_not_required from django.contrib.gis.db.models.functions import Distance from django.db.models import Case, F, Prefetch, Q, When from django.shortcuts import render @@ -12,6 +13,7 @@ from itou.companies.models import Company, JobDescription from itou.job_applications.models import JobApplication, JobApplicationWorkflow from itou.prescribers.models import PrescriberOrganization +from itou.utils.auth import LoginNotRequiredMixin from itou.utils.pagination import pager from itou.utils.urls import add_url_params from itou.www.apply.views.submit_views import ApplyForJobSeekerMixin @@ -25,12 +27,13 @@ PageAndCounts = namedtuple("PageAndCounts", ("results_page", "siaes_count", "job_descriptions_count")) +@login_not_required def employer_search_home(request, template_name="search/siaes_search_home.html"): context = {"siae_search_form": SiaeSearchForm()} return render(request, template_name, context) -class EmployerSearchBaseView(ApplyForJobSeekerMixin, FormView): +class EmployerSearchBaseView(LoginNotRequiredMixin, ApplyForJobSeekerMixin, FormView): form_class = SiaeSearchForm initial = {"distance": SiaeSearchForm.DISTANCE_DEFAULT} @@ -276,6 +279,7 @@ def get_results_page_and_counts(self, siaes, job_descriptions): ) +@login_not_required def search_prescribers_home(request, template_name="search/prescribers_search_home.html"): """ The search home page has a different design from the results page. @@ -285,6 +289,7 @@ def search_prescribers_home(request, template_name="search/prescribers_search_ho return render(request, template_name, context) +@login_not_required def search_prescribers_results(request, template_name="search/prescribers_search_results.html"): city = None distance = None diff --git a/itou/www/siae_evaluations_views/views.py b/itou/www/siae_evaluations_views/views.py index cfad55db77a..7ed02a52059 100644 --- a/itou/www/siae_evaluations_views/views.py +++ b/itou/www/siae_evaluations_views/views.py @@ -1,5 +1,5 @@ from django.contrib import messages -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_not_required, login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.core.files.storage import default_storage from django.db.models import Q @@ -741,5 +741,6 @@ def view_proof(request, evaluated_administrative_criteria_id): return HttpResponseRedirect(default_storage.url(criteria.proof_id)) +@login_not_required def sanctions_helper_view(request): return render(request, "siae_evaluations/sanctions_helper.html") diff --git a/itou/www/signup/urls.py b/itou/www/signup/urls.py index 4b49449b8d0..44f8f55bc99 100644 --- a/itou/www/signup/urls.py +++ b/itou/www/signup/urls.py @@ -1,3 +1,4 @@ +from django.contrib.auth.decorators import login_not_required from django.urls import path, re_path from django.views.generic import TemplateView @@ -15,7 +16,7 @@ path("job_seeker/situation", views.job_seeker_situation, name="job_seeker_situation"), path( "job_seeker/situation_not_eligible", - TemplateView.as_view(template_name="signup/job_seeker_situation_not_eligible.html"), + login_not_required(TemplateView.as_view(template_name="signup/job_seeker_situation_not_eligible.html")), name="job_seeker_situation_not_eligible", ), path( diff --git a/itou/www/signup/views.py b/itou/www/signup/views.py index ac359aaf0c3..d0c64f17dcc 100644 --- a/itou/www/signup/views.py +++ b/itou/www/signup/views.py @@ -9,7 +9,7 @@ from django.conf import settings from django.contrib import auth, messages from django.contrib.auth import REDIRECT_FIELD_NAME, login -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_not_required, login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.db import Error, transaction @@ -28,6 +28,7 @@ from itou.users.adapter import UserAdapter from itou.users.enums import KIND_EMPLOYER, KIND_PRESCRIBER, MATOMO_ACCOUNT_TYPE, UserKind from itou.utils import constants as global_constants +from itou.utils.auth import LoginNotRequiredMixin from itou.utils.nav_history import get_prev_url_from_history, push_url_in_history from itou.utils.tokens import company_signup_token_generator from itou.utils.urls import get_safe_url @@ -81,6 +82,7 @@ def get_success_url(self): return super().get_success_url() +@login_not_required @require_GET def signup(request, template_name="signup/signup.html"): """ @@ -93,7 +95,7 @@ def signup(request, template_name="signup/signup.html"): return render(request, template_name, context) -class ChooseUserKindSignupView(FormView): +class ChooseUserKindSignupView(LoginNotRequiredMixin, FormView): template_name = "signup/choose_user_kind.html" form_class = forms.ChooseUserKindSignupForm @@ -106,7 +108,7 @@ def form_valid(self, form): return HttpResponseRedirect(urls[form.cleaned_data["kind"]]) -class JobSeekerSignupView(SignupView): +class JobSeekerSignupView(LoginNotRequiredMixin, SignupView): form_class = forms.JobSeekerSignupForm template_name = "signup/job_seeker_signup.html" @@ -122,6 +124,7 @@ def get_form_kwargs(self): return kwargs +@login_not_required def job_seeker_situation(request, template_name="signup/job_seeker_situation.html"): """ Second step of the signup process for jobseeker. @@ -151,6 +154,7 @@ def job_seeker_situation(request, template_name="signup/job_seeker_situation.htm return render(request, template_name, context) +@login_not_required def job_seeker_nir(request, template_name="signup/job_seeker_nir.html"): form = forms.JobSeekerNirForm(data=request.POST or None) @@ -175,6 +179,7 @@ def job_seeker_nir(request, template_name="signup/job_seeker_nir.html"): # ------------------------------------------------------------------------------------------ +@login_not_required def company_select(request, template_name="signup/company_select.html"): """ Entry point of the signup process for SIAEs which consists of 2 steps. @@ -257,7 +262,7 @@ def dispatch(self, request, *args, company_id, **kwargs): return super().dispatch(request, *args, **kwargs) -class CompanyUserView(CompanyBaseView, TemplateView): +class CompanyUserView(LoginNotRequiredMixin, CompanyBaseView, TemplateView): """ Display Inclusion Connect button. This page is also shown if an error is detected during @@ -324,6 +329,7 @@ def decorated(request, *args, **kwargs): return decorated +@login_not_required def prescriber_check_already_exists(request, template_name="signup/prescriber_check_already_exists.html"): """ @@ -393,6 +399,7 @@ def prescriber_check_already_exists(request, template_name="signup/prescriber_ch return render(request, template_name, context) +@login_not_required @valid_prescriber_signup_session_required @push_url_in_history(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY) def prescriber_request_invitation(request, membership_id, template_name="signup/prescriber_request_invitation.html"): @@ -428,6 +435,7 @@ def prescriber_request_invitation(request, membership_id, template_name="signup/ return render(request, template_name, context) +@login_not_required @valid_prescriber_signup_session_required @push_url_in_history(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY) def prescriber_choose_org(request, siret, template_name="signup/prescriber_choose_org.html"): @@ -473,6 +481,7 @@ def prescriber_choose_org(request, siret, template_name="signup/prescriber_choos return render(request, template_name, context) +@login_not_required @valid_prescriber_signup_session_required @push_url_in_history(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY) def prescriber_choose_kind(request, template_name="signup/prescriber_choose_kind.html"): @@ -517,6 +526,7 @@ def prescriber_choose_kind(request, template_name="signup/prescriber_choose_kind return render(request, template_name, context) +@login_not_required @valid_prescriber_signup_session_required @push_url_in_history(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY) def prescriber_confirm_authorization(request, template_name="signup/prescriber_confirm_authorization.html"): @@ -551,6 +561,7 @@ def prescriber_confirm_authorization(request, template_name="signup/prescriber_c return render(request, template_name, context) +@login_not_required @valid_prescriber_signup_session_required @push_url_in_history(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY) def prescriber_pole_emploi_safir_code(request, template_name="signup/prescriber_pole_emploi_safir_code.html"): @@ -583,6 +594,7 @@ def prescriber_pole_emploi_safir_code(request, template_name="signup/prescriber_ return render(request, template_name, context) +@login_not_required @valid_prescriber_signup_session_required @push_url_in_history(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY) def prescriber_check_pe_email(request, template_name="signup/prescriber_check_pe_email.html"): @@ -612,6 +624,7 @@ def prescriber_check_pe_email(request, template_name="signup/prescriber_check_pe return render(request, template_name, context) +@login_not_required @valid_prescriber_signup_session_required @push_url_in_history(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY) def prescriber_pole_emploi_user(request, template_name="signup/prescriber_pole_emploi_user.html"): @@ -651,6 +664,7 @@ def prescriber_pole_emploi_user(request, template_name="signup/prescriber_pole_e return render(request, template_name, context) +@login_not_required @valid_prescriber_signup_session_required @push_url_in_history(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY) def prescriber_user(request, template_name="signup/prescriber_user.html"): @@ -821,6 +835,7 @@ def _get_session_siae(self): ) +@login_not_required def facilitator_search(request, template_name="signup/facilitator_search.html"): form = forms.FacilitatorSearchForm(data=request.POST or None) if request.method == "POST" and form.is_valid(): @@ -834,7 +849,7 @@ def facilitator_search(request, template_name="signup/facilitator_search.html"): return render(request, template_name, context) -class FacilitatorUserView(FacilitatorBaseMixin, TemplateView): +class FacilitatorUserView(LoginNotRequiredMixin, FacilitatorBaseMixin, TemplateView): """ Display Inclusion Connect button. This page is also shown if an error is detected during @@ -865,7 +880,7 @@ def get_context_data(self, **kwargs): } -class FacilitatorJoinView(FacilitatorBaseMixin, View): +class FacilitatorJoinView(LoginNotRequiredMixin, FacilitatorBaseMixin, View): def get(self, request, *args, **kwargs): self.company_to_create.auth_email = request.user.email self.company_to_create.created_by = request.user diff --git a/itou/www/stats/views.py b/itou/www/stats/views.py index fc1c5cf81e7..029021c4604 100644 --- a/itou/www/stats/views.py +++ b/itou/www/stats/views.py @@ -15,7 +15,7 @@ """ from django.conf import settings -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_not_required, login_required from django.core.exceptions import PermissionDenied from django.http import HttpResponseNotFound, HttpResponseRedirect from django.shortcuts import render @@ -175,6 +175,7 @@ def render_stats(request, context, params=None, template_name="stats/stats.html" return render(request, template_name, base_context) +@login_not_required def stats_public(request): """ Public basic stats (signed and embedded version) diff --git a/tests/www/apply/test_submit.py b/tests/www/apply/test_submit.py index cec426e8bca..549c05683b1 100644 --- a/tests/www/apply/test_submit.py +++ b/tests/www/apply/test_submit.py @@ -3783,15 +3783,15 @@ def test_anonymous_step_1(self, client): def test_anonymous_step_2(self, client): response = client.get(self.step_2_url) - assert response.status_code == 403 + assertRedirects(response, reverse("account_login") + f"?next={self.step_2_url}") def test_anonymous_step_3(self, client): response = client.get(self.step_3_url) - assert response.status_code == 403 + assertRedirects(response, reverse("account_login") + f"?next={self.step_3_url}") def test_anonymous_step_end(self, client): response = client.get(self.step_end_url) - assert response.status_code == 403 + assertRedirects(response, reverse("account_login") + f"?next={self.step_end_url}") def test_as_job_seeker(self, client): self._check_nothing_permitted(client, self.job_seeker) @@ -3938,15 +3938,15 @@ def test_anonymous_step_1(self, client): def test_anonymous_step_2(self, client): response = client.get(self.step_2_url) - assert response.status_code == 403 + assertRedirects(response, reverse("account_login") + f"?next={self.step_2_url}") def test_anonymous_step_3(self, client): response = client.get(self.step_3_url) - assert response.status_code == 403 + assertRedirects(response, reverse("account_login") + f"?next={self.step_3_url}") def test_anonymous_step_end(self, client): response = client.get(self.step_end_url) - assert response.status_code == 403 + assertRedirects(response, reverse("account_login") + f"?next={self.step_end_url}") def test_as_job_seeker(self, client): self._check_nothing_permitted(client, self.job_seeker) diff --git a/tests/www/signup/test_prescriber.py b/tests/www/signup/test_prescriber.py index 2024295c176..2a79e13891c 100644 --- a/tests/www/signup/test_prescriber.py +++ b/tests/www/signup/test_prescriber.py @@ -1213,9 +1213,14 @@ def test_permission_denied_when_skiping_first_step(self, client, subtests): reverse("signup:prescriber_check_pe_email"), reverse("signup:prescriber_pole_emploi_user"), reverse("signup:prescriber_user"), - reverse("signup:prescriber_join_org"), ] for url in urls: with subtests.test(url=url): response = client.get(url) assert response.status_code == 403 + # This view requires to be logged in + url = reverse("signup:prescriber_join_org") + with subtests.test(url=url): + client.force_login(PrescriberFactory()) + response = client.get(url) + assert response.status_code == 403