From 851db4166585d428f071f5d052970e9a8acb3a25 Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 20 May 2025 21:04:49 +0200 Subject: [PATCH] adapt `CanAccessLookup` to api key auth --- club/api.py | 7 ++++-- club/tests/test_club_controller.py | 15 ++++++++----- core/api.py | 4 ++++ core/auth/api_permissions.py | 2 +- core/management/commands/populate.py | 2 ++ core/migrations/0046_permissionrights.py | 28 ++++++++++++++++++++++++ core/models.py | 17 ++++++++++++++ counter/api.py | 4 ++++ pedagogy/tests/test_api.py | 2 +- sas/api.py | 3 +++ 10 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 core/migrations/0046_permissionrights.py diff --git a/club/api.py b/club/api.py index 147f6379..decdf8f8 100644 --- a/club/api.py +++ b/club/api.py @@ -8,7 +8,7 @@ from ninja_extra.schemas import PaginatedResponseSchema from apikey.auth import ApiKeyAuth from club.models import Club -from club.schemas import ClubSchema +from club.schemas import ClubSchema, SimpleClubSchema from core.auth.api_permissions import CanAccessLookup, HasPerm @@ -16,8 +16,10 @@ from core.auth.api_permissions import CanAccessLookup, HasPerm class ClubController(ControllerBase): @route.get( "/search", - response=PaginatedResponseSchema[ClubSchema], + response=PaginatedResponseSchema[SimpleClubSchema], + auth=[SessionAuth(), ApiKeyAuth()], permissions=[CanAccessLookup], + url_name="search_club", ) @paginate(PageNumberPaginationExtra, page_size=50) def search_club(self, search: Annotated[str, MinLen(1)]): @@ -28,6 +30,7 @@ class ClubController(ControllerBase): response=ClubSchema, auth=[SessionAuth(), ApiKeyAuth()], permissions=[HasPerm("club.view_club")], + url_name="fetch_club", ) def fetch_club(self, club_id: int): return self.get_object_or_exception( diff --git a/club/tests/test_club_controller.py b/club/tests/test_club_controller.py index e48a4513..ade8eb4d 100644 --- a/club/tests/test_club_controller.py +++ b/club/tests/test_club_controller.py @@ -1,16 +1,21 @@ import pytest +from django.test import Client +from django.urls import reverse from model_bakery import baker -from ninja_extra.testing import TestClient from pytest_django.asserts import assertNumQueries -from club.api import ClubController from club.models import Club, Membership +from core.baker_recipes import subscriber_user @pytest.mark.django_db -def test_fetch_club(): +def test_fetch_club(client: Client): club = baker.make(Club) baker.make(Membership, club=club, _quantity=10, _bulk_create=True) - with assertNumQueries(3): - res = TestClient(ClubController).get(f"/{club.id}") + user = subscriber_user.make() + client.force_login(user) + with assertNumQueries(7): + # - 4 queries for authentication + # - 3 queries for the actual data + res = client.get(reverse("api:fetch_club", kwargs={"club_id": club.id})) assert res.status_code == 200 diff --git a/core/api.py b/core/api.py index 830e06e9..bd74875b 100644 --- a/core/api.py +++ b/core/api.py @@ -5,11 +5,13 @@ from django.conf import settings from django.db.models import F from django.http import HttpResponse from ninja import File, Query +from ninja.security import SessionAuth from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra.exceptions import PermissionDenied from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.schemas import PaginatedResponseSchema +from apikey.auth import ApiKeyAuth from club.models import Mailing from core.auth.api_permissions import CanAccessLookup, CanView, HasPerm from core.models import Group, QuickUploadImage, SithFile, User @@ -90,6 +92,7 @@ class SithFileController(ControllerBase): @route.get( "/search", response=PaginatedResponseSchema[SithFileSchema], + auth=[SessionAuth(), ApiKeyAuth()], permissions=[CanAccessLookup], ) @paginate(PageNumberPaginationExtra, page_size=50) @@ -102,6 +105,7 @@ class GroupController(ControllerBase): @route.get( "/search", response=PaginatedResponseSchema[GroupSchema], + auth=[SessionAuth(), ApiKeyAuth()], permissions=[CanAccessLookup], ) @paginate(PageNumberPaginationExtra, page_size=50) diff --git a/core/auth/api_permissions.py b/core/auth/api_permissions.py index 3d18529e..73f9fa84 100644 --- a/core/auth/api_permissions.py +++ b/core/auth/api_permissions.py @@ -189,4 +189,4 @@ class IsLoggedInCounter(BasePermission): return Counter.objects.filter(token=token).exists() -CanAccessLookup = IsOldSubscriber | IsRoot | IsLoggedInCounter +CanAccessLookup = IsLoggedInCounter | HasPerm("core.access_lookup") diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index ea1f0342..8f101d9f 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -805,6 +805,8 @@ class Command(BaseCommand): "add_peoplepicturerelation", "add_page", "add_quickuploadimage", + "view_club", + "access_lookup", ] ) ) diff --git a/core/migrations/0046_permissionrights.py b/core/migrations/0046_permissionrights.py new file mode 100644 index 00000000..33df716b --- /dev/null +++ b/core/migrations/0046_permissionrights.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2 on 2025-05-20 17:50 +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("core", "0045_quickuploadimage")] + + operations = [ + migrations.CreateModel( + name="GlobalPermissionRights", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={ + "permissions": [("access_lookup", "Can access any lookup in the sith")], + "managed": False, + "default_permissions": [], + }, + ), + ] diff --git a/core/models.py b/core/models.py index ce4b2102..9dc2f236 100644 --- a/core/models.py +++ b/core/models.py @@ -754,6 +754,23 @@ class UserBan(models.Model): return f"Ban of user {self.user.id}" +class GlobalPermissionRights(models.Model): + """Little hack to have permissions not linked to a specific db table.""" + + class Meta: + # No database table creation or deletion + # operations will be performed for this model. + managed = False + + # disable "add", "change", "delete" and "view" default permissions + default_permissions = [] + + permissions = [("access_lookup", "Can access any lookup in the sith")] + + def __str__(self): + return self.__class__.__name__ + + class Preferences(models.Model): user = models.OneToOneField( User, related_name="_preferences", on_delete=models.CASCADE diff --git a/counter/api.py b/counter/api.py index 44b58488..11bf56c9 100644 --- a/counter/api.py +++ b/counter/api.py @@ -16,10 +16,12 @@ from django.conf import settings from django.db.models import F from django.shortcuts import get_object_or_404 from ninja import Query +from ninja.security import SessionAuth from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.schemas import PaginatedResponseSchema +from apikey.auth import ApiKeyAuth from core.auth.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot from counter.models import Counter, Product, ProductType from counter.schemas import ( @@ -62,6 +64,7 @@ class CounterController(ControllerBase): @route.get( "/search", response=PaginatedResponseSchema[SimplifiedCounterSchema], + auth=[SessionAuth(), ApiKeyAuth()], permissions=[CanAccessLookup], ) @paginate(PageNumberPaginationExtra, page_size=50) @@ -74,6 +77,7 @@ class ProductController(ControllerBase): @route.get( "/search", response=PaginatedResponseSchema[SimpleProductSchema], + auth=[SessionAuth(), ApiKeyAuth()], permissions=[CanAccessLookup], ) @paginate(PageNumberPaginationExtra, page_size=50) diff --git a/pedagogy/tests/test_api.py b/pedagogy/tests/test_api.py index cbb99c18..a95acf20 100644 --- a/pedagogy/tests/test_api.py +++ b/pedagogy/tests/test_api.py @@ -68,7 +68,7 @@ class TestUVSearch(TestCase): def test_permissions(self): # Test with anonymous user response = self.client.get(self.url) - assert response.status_code == 403 + assert response.status_code == 401 # Test with not subscribed user self.client.force_login(baker.make(User)) diff --git a/sas/api.py b/sas/api.py index b82ff5e1..83d7f0c8 100644 --- a/sas/api.py +++ b/sas/api.py @@ -5,6 +5,7 @@ from django.core.exceptions import ValidationError from django.db.models import F from django.urls import reverse from ninja import Body, File, Query +from ninja.security import SessionAuth from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra.exceptions import NotFound, PermissionDenied from ninja_extra.pagination import PageNumberPaginationExtra @@ -12,6 +13,7 @@ from ninja_extra.permissions import IsAuthenticated from ninja_extra.schemas import PaginatedResponseSchema from pydantic import NonNegativeInt +from apikey.auth import ApiKeyAuth from core.auth.api_permissions import ( CanAccessLookup, CanEdit, @@ -53,6 +55,7 @@ class AlbumController(ControllerBase): @route.get( "/autocomplete-search", response=PaginatedResponseSchema[AlbumAutocompleteSchema], + auth=[SessionAuth(), ApiKeyAuth()], permissions=[CanAccessLookup], ) @paginate(PageNumberPaginationExtra, page_size=50)