remove deprecated api csrf argument

This commit is contained in:
imperosol
2025-11-09 12:46:22 +01:00
parent 02f7e10729
commit 2e9e1b6a78
10 changed files with 66 additions and 44 deletions

View File

@@ -6,6 +6,8 @@ from api.models import ApiClient, ApiKey
class ApiKeyAuth(APIKeyHeader): class ApiKeyAuth(APIKeyHeader):
"""Authentication through client api keys."""
param_name = "X-APIKey" param_name = "X-APIKey"
def authenticate(self, request: HttpRequest, key: str | None) -> ApiClient | None: def authenticate(self, request: HttpRequest, key: str | None) -> ApiClient | None:

View File

@@ -1,3 +1,4 @@
from ninja.security import SessionAuth
from ninja_extra import NinjaExtraAPI from ninja_extra import NinjaExtraAPI
api = NinjaExtraAPI( api = NinjaExtraAPI(
@@ -5,6 +6,6 @@ api = NinjaExtraAPI(
description="Portail Interactif de Communication avec les Outils Numériques", description="Portail Interactif de Communication avec les Outils Numériques",
version="0.2.0", version="0.2.0",
urls_namespace="api", urls_namespace="api",
csrf=True, auth=[SessionAuth()],
) )
api.auto_discover_controllers() api.auto_discover_controllers()

View File

@@ -5,7 +5,6 @@ from django.utils.cache import add_never_cache_headers
from ninja import Query from ninja import Query
from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.permissions import IsAuthenticated
from ninja_extra.schemas import PaginatedResponseSchema from ninja_extra.schemas import PaginatedResponseSchema
from api.permissions import HasPerm from api.permissions import HasPerm
@@ -17,17 +16,13 @@ from core.views.files import send_raw_file
@api_controller("/calendar") @api_controller("/calendar")
class CalendarController(ControllerBase): class CalendarController(ControllerBase):
@route.get("/internal.ics", url_name="calendar_internal") @route.get("/internal.ics", auth=None, url_name="calendar_internal")
def calendar_internal(self): def calendar_internal(self):
response = send_raw_file(IcsCalendar.get_internal()) response = send_raw_file(IcsCalendar.get_internal())
add_never_cache_headers(response) add_never_cache_headers(response)
return response return response
@route.get( @route.get("/unpublished.ics", url_name="calendar_unpublished")
"/unpublished.ics",
permissions=[IsAuthenticated],
url_name="calendar_unpublished",
)
def calendar_unpublished(self): def calendar_unpublished(self):
response = HttpResponse( response = HttpResponse(
IcsCalendar.get_unpublished(self.context.request.user), IcsCalendar.get_unpublished(self.context.request.user),
@@ -74,6 +69,7 @@ class NewsController(ControllerBase):
@route.get( @route.get(
"/date", "/date",
auth=None,
url_name="fetch_news_dates", url_name="fetch_news_dates",
response=PaginatedResponseSchema[NewsDateSchema], response=PaginatedResponseSchema[NewsDateSchema],
) )

View File

@@ -46,7 +46,7 @@ class TestFetchFamilyApi(TestCase):
response = self.client.get( response = self.client.get(
reverse("api:family_graph", args=[self.main_user.id]) reverse("api:family_graph", args=[self.main_user.id])
) )
assert response.status_code == 403 assert response.status_code == 401
self.client.force_login(baker.make(User)) # unsubscribed user self.client.force_login(baker.make(User)) # unsubscribed user
response = self.client.get( response = self.client.get(

View File

@@ -269,7 +269,7 @@ def test_apply_rights_recursively():
SimpleUploadedFile( SimpleUploadedFile(
"test.jpg", content=RED_PIXEL_PNG, content_type="image/jpg" "test.jpg", content=RED_PIXEL_PNG, content_type="image/jpg"
), ),
403, 401,
), ),
( (
lambda: baker.make(User), lambda: baker.make(User),

View File

@@ -117,7 +117,7 @@ class ProductTypeController(ControllerBase):
def fetch_all(self): def fetch_all(self):
return ProductType.objects.order_by("order") return ProductType.objects.order_by("order")
@route.patch("/{type_id}/move") @route.patch("/{type_id}/move", url_name="reorder_product_type")
def reorder(self, type_id: int, other_id: Query[ReorderProductTypeSchema]): def reorder(self, type_id: int, other_id: Query[ReorderProductTypeSchema]):
"""Change the order of a product type. """Change the order of a product type.

View File

@@ -3,11 +3,9 @@ from django.conf import settings
from django.test import Client from django.test import Client
from django.urls import reverse from django.urls import reverse
from model_bakery import baker, seq from model_bakery import baker, seq
from ninja_extra.testing import TestClient
from core.baker_recipes import board_user, subscriber_user from core.baker_recipes import board_user, subscriber_user
from core.models import Group, User from core.models import Group, User
from counter.api import ProductTypeController
from counter.models import ProductType from counter.models import ProductType
@@ -19,24 +17,43 @@ def product_types(db) -> list[ProductType]:
return baker.make(ProductType, _quantity=5, order=seq(0)) return baker.make(ProductType, _quantity=5, order=seq(0))
@pytest.fixture()
def counter_admin_client(db, client: Client) -> Client:
client.force_login(
baker.make(
User, groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)]
)
)
return client
@pytest.mark.django_db @pytest.mark.django_db
def test_fetch_product_types(product_types: list[ProductType]): def test_fetch_product_types(
counter_admin_client: Client, product_types: list[ProductType]
):
"""Test that the API returns the right products in the right order""" """Test that the API returns the right products in the right order"""
client = TestClient(ProductTypeController) response = counter_admin_client.get(reverse("api:fetch_product_types"))
response = client.get("")
assert response.status_code == 200 assert response.status_code == 200
assert [i["id"] for i in response.json()] == [t.id for t in product_types] assert [i["id"] for i in response.json()] == [t.id for t in product_types]
@pytest.mark.django_db @pytest.mark.django_db
def test_move_below_product_type(product_types: list[ProductType]): def test_move_below_product_type(
counter_admin_client: Client, product_types: list[ProductType]
):
"""Test that moving a product below another works""" """Test that moving a product below another works"""
client = TestClient(ProductTypeController) response = counter_admin_client.patch(
response = client.patch( reverse(
f"/{product_types[-1].id}/move", query={"below": product_types[0].id} "api:reorder_product_type",
kwargs={"type_id": product_types[-1].id},
query={"below": product_types[0].id},
),
) )
assert response.status_code == 200 assert response.status_code == 200
new_order = [i["id"] for i in client.get("").json()] new_order = [
i["id"]
for i in counter_admin_client.get(reverse("api:fetch_product_types")).json()
]
assert new_order == [ assert new_order == [
product_types[0].id, product_types[0].id,
product_types[-1].id, product_types[-1].id,
@@ -45,14 +62,22 @@ def test_move_below_product_type(product_types: list[ProductType]):
@pytest.mark.django_db @pytest.mark.django_db
def test_move_above_product_type(product_types: list[ProductType]): def test_move_above_product_type(
counter_admin_client: Client, product_types: list[ProductType]
):
"""Test that moving a product above another works""" """Test that moving a product above another works"""
client = TestClient(ProductTypeController) response = counter_admin_client.patch(
response = client.patch( reverse(
f"/{product_types[1].id}/move", query={"above": product_types[0].id} "api:reorder_product_type",
kwargs={"type_id": product_types[1].id},
query={"above": product_types[0].id},
),
) )
assert response.status_code == 200 assert response.status_code == 200
new_order = [i["id"] for i in client.get("").json()] new_order = [
i["id"]
for i in counter_admin_client.get(reverse("api:fetch_product_types")).json()
]
assert new_order == [ assert new_order == [
product_types[1].id, product_types[1].id,
product_types[0].id, product_types[0].id,

View File

@@ -49,8 +49,9 @@ Notre API offre deux moyens d'authentification :
- par clef d'API - par clef d'API
La plus grande partie des routes de l'API utilisent la méthode par cookie de session. La plus grande partie des routes de l'API utilisent la méthode par cookie de session.
Cette dernière est donc activée par défaut.
Pour placer une route d'API derrière l'une de ces méthodes (ou bien les deux), Pour changer la méthode d'authentification,
utilisez l'attribut `auth` et les classes `SessionAuth` et utilisez l'attribut `auth` et les classes `SessionAuth` et
[`ApiKeyAuth`][api.auth.ApiKeyAuth]. [`ApiKeyAuth`][api.auth.ApiKeyAuth].
@@ -60,13 +61,17 @@ utilisez l'attribut `auth` et les classes `SessionAuth` et
@api_controller("/foo") @api_controller("/foo")
class FooController(ControllerBase): class FooController(ControllerBase):
# Cette route sera accessible uniquement avec l'authentification # Cette route sera accessible uniquement avec l'authentification
# par cookie de session # par clef d'API
@route.get("", auth=[SessionAuth()]) @route.get("", auth=[ApiKeyAuth()])
def fetch_foo(self, club_id: int): ... def fetch_foo(self, club_id: int): ...
# Et celle-ci sera accessible peut importe la méthode d'authentification # Celle-ci sera accessible peu importe la méthode d'authentification
@route.get("/bar", auth=[SessionAuth(), ApiKeyAuth()]) @route.get("/bar", auth=[SessionAuth(), ApiKeyAuth()])
def fetch_bar(self, club_id: int): ... def fetch_bar(self, club_id: int): ...
# Et celle-ci sera accessible aussi aux utilisateurs non-connectés
@route.get("/public", auth=None)
def fetch_public(self, club_id: int): ...
``` ```
### Permissions ### Permissions
@@ -123,7 +128,7 @@ Ceux-ci incluent notamment un système de
à fournir dans les requêtes POST/PUT/PATCH. à fournir dans les requêtes POST/PUT/PATCH.
Ceux-ci sont bien adaptés au cycle requêtes/réponses Ceux-ci sont bien adaptés au cycle requêtes/réponses
typique de l'expérience utilisateur sur un navigateur, typiques de l'expérience utilisateur sur un navigateur,
où les requêtes POST sont toujours effectuées après une requête où les requêtes POST sont toujours effectuées après une requête
GET au cours de laquelle on a pu récupérer un token csrf. GET au cours de laquelle on a pu récupérer un token csrf.
Cependant, le flux des requêtes sur une API est bien différent ; Cependant, le flux des requêtes sur une API est bien différent ;

View File

@@ -8,7 +8,6 @@ from ninja.security import SessionAuth
from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.exceptions import NotFound, PermissionDenied from ninja_extra.exceptions import NotFound, PermissionDenied
from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.permissions import IsAuthenticated
from ninja_extra.schemas import PaginatedResponseSchema from ninja_extra.schemas import PaginatedResponseSchema
from pydantic import NonNegativeInt from pydantic import NonNegativeInt
@@ -41,7 +40,6 @@ class AlbumController(ControllerBase):
@route.get( @route.get(
"/search", "/search",
response=PaginatedResponseSchema[AlbumSchema], response=PaginatedResponseSchema[AlbumSchema],
permissions=[IsAuthenticated],
url_name="search-album", url_name="search-album",
) )
@paginate(PageNumberPaginationExtra, page_size=50) @paginate(PageNumberPaginationExtra, page_size=50)
@@ -74,12 +72,7 @@ class AlbumController(ControllerBase):
@api_controller("/sas/picture") @api_controller("/sas/picture")
class PicturesController(ControllerBase): class PicturesController(ControllerBase):
@route.get( @route.get("", response=PaginatedResponseSchema[PictureSchema], url_name="pictures")
"",
response=PaginatedResponseSchema[PictureSchema],
permissions=[IsAuthenticated],
url_name="pictures",
)
@paginate(PageNumberPaginationExtra, page_size=100) @paginate(PageNumberPaginationExtra, page_size=100)
def fetch_pictures(self, filters: Query[PictureFilterSchema]): def fetch_pictures(self, filters: Query[PictureFilterSchema]):
"""Find pictures viewable by the user corresponding to the given filters. """Find pictures viewable by the user corresponding to the given filters.
@@ -141,7 +134,7 @@ class PicturesController(ControllerBase):
@route.get( @route.get(
"/{picture_id}/identified", "/{picture_id}/identified",
permissions=[IsAuthenticated, CanView], permissions=[CanView],
response=list[IdentifiedUserSchema], response=list[IdentifiedUserSchema],
) )
def fetch_identifications(self, picture_id: int): def fetch_identifications(self, picture_id: int):
@@ -149,7 +142,7 @@ class PicturesController(ControllerBase):
picture = self.get_object_or_exception(Picture, pk=picture_id) picture = self.get_object_or_exception(Picture, pk=picture_id)
return picture.people.select_related("user") return picture.people.select_related("user")
@route.put("/{picture_id}/identified", permissions=[IsAuthenticated, CanView]) @route.put("/{picture_id}/identified", permissions=[CanView])
def identify_users(self, picture_id: NonNegativeInt, users: set[NonNegativeInt]): def identify_users(self, picture_id: NonNegativeInt, users: set[NonNegativeInt]):
picture = self.get_object_or_exception( picture = self.get_object_or_exception(
Picture.objects.select_related("parent"), pk=picture_id Picture.objects.select_related("parent"), pk=picture_id
@@ -209,7 +202,7 @@ class PicturesController(ControllerBase):
@api_controller("/sas/relation", tags="User identification on SAS pictures") @api_controller("/sas/relation", tags="User identification on SAS pictures")
class UsersIdentifiedController(ControllerBase): class UsersIdentifiedController(ControllerBase):
@route.delete("/{relation_id}", permissions=[IsAuthenticated]) @route.delete("/{relation_id}")
def delete_relation(self, relation_id: NonNegativeInt): def delete_relation(self, relation_id: NonNegativeInt):
"""Untag a user from a SAS picture. """Untag a user from a SAS picture.

View File

@@ -55,7 +55,7 @@ class TestPictureSearch(TestSas):
def test_anonymous_user_forbidden(self): def test_anonymous_user_forbidden(self):
res = self.client.get(self.url) res = self.client.get(self.url)
assert res.status_code == 403 assert res.status_code == 401
def test_filter_by_album(self): def test_filter_by_album(self):
self.client.force_login(self.user_b) self.client.force_login(self.user_b)
@@ -148,7 +148,7 @@ class TestPictureRelation(TestSas):
relation = PeoplePictureRelation.objects.exclude(user=self.user_a).first() relation = PeoplePictureRelation.objects.exclude(user=self.user_a).first()
res = self.client.delete(f"/api/sas/relation/{relation.id}") res = self.client.delete(f"/api/sas/relation/{relation.id}")
assert res.status_code == 403 assert res.status_code == 401
for user in baker.make(User), self.user_a: for user in baker.make(User), self.user_a:
self.client.force_login(user) self.client.force_login(user)