List visibility restricts the candidate records a principal can see before ordering and pagination select the returned page.
Django REST Framework list visibility: filter the principal’s rows before paging
Operation contract
The local API view requires an authenticated principal, then applies the owner filter to its owned receipt records before the cursor and limit. Another owner’s row cannot appear in a page even if its ID lies between visible records. The cursor is an exclusive numeric ID and each response contains at most two records. The view never relies on detail-object checks being called for list results.
Failure and ownership boundary
Test authentication is bypassed with force_authenticate, so this fixture does not implement tokens or login. Real querysets must place tenant/owner predicates in the database query before paging; filtering only after a page has been fetched can leak data or skip visible rows. A received owner_id must not choose the principal. Django REST Framework object permissions: check the retrieved record, not only login, Django keyset pagination: stable ordering and a bounded next page and Spring method authorization: test the proxied service boundary address other layers.
Tested environment
Dependency check: this program was executed on CPython 3.14.6 with Django==5.2.17, djangorestframework==3.18.1. Install these versions in a separate virtual environment. The download includes the recorded environment snapshot; no third-party package is part of the website runtime.
Working program
from types import SimpleNamespace
from django.conf import settings
settings.configure(INSTALLED_APPS=[], SECRET_KEY="owned-list-fixture", REST_FRAMEWORK={"DEFAULT_AUTHENTICATION_CLASSES": [], "UNAUTHENTICATED_USER": None})
import django
django.setup()
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.test import APIRequestFactory, force_authenticate
RECEIPTS = [{"id": 41, "owner_id": 7}, {"id": 42, "owner_id": 8}, {"id": 43, "owner_id": 7}, {"id": 44, "owner_id": 7}]
def visible_receipts(principal, after, limit):
if type(principal) is not int or principal <= 0 or type(after) is not int or after < 0 or type(limit) is not int or not 1 <= limit <= 2:
raise ValueError("list boundary rejected")
candidates = sorted((record for record in RECEIPTS if record["owner_id"] == principal and record["id"] > after), key=lambda record: record["id"])
return [{"id": record["id"]} for record in candidates[:limit]]
class ReceiptList(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
return Response(visible_receipts(request.user.pk, 0, 2))
request = APIRequestFactory().get("/receipts")
force_authenticate(request, user=SimpleNamespace(pk=7, is_authenticated=True))
print("owner page:", ReceiptList.as_view()(request).data)
print("next owner page:", visible_receipts(7, 43, 2))
print("other owner page:", visible_receipts(8, 0, 2))
print("anonymous:", ReceiptList.as_view()(APIRequestFactory().get("/receipts")).status_code)Output
owner page: [{'id': 41}, {'id': 43}]
next owner page: [{'id': 44}]
other owner page: [{'id': 42}]
anonymous: 403Costs and limits
The local list scan/sort costs O(n log n) before its bounded returned page. Production query cost depends on owner/cursor indexes and the database plan. A returned page cap does not bound the cost of an unindexed scan.
Common Mistakes
- Apply visibility predicates before selecting a page.
- Detail-object permission hooks do not automatically filter list rows.
Connected lessons
Django REST Framework object permissions: check the retrieved record, not only login, Django keyset pagination: stable ordering and a bounded next page, Spring method authorization: test the proxied service boundary.
Follow the service contract
Python Django database constraints: enforce receipt identity per owner.
Follow the ownership and update boundary
Python Flask ETag responses: preserve cache validators across conditional reads.
