Typorithm · medium

Scope lists and check individual object access

pythondjangodrfindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Scope lists and check individual object access

0%

Concept and contract

Keep collection visibility, identity and object permissions distinct.

A user can be authenticated yet unauthorized to read another owner record. A collection has an additional problem: object permission hooks do not automatically run for every listed row. Scope the queryset first, then keep object checks where individual records are retrieved.

The fixture has two real owned users and four technical roles. A shared scope mixin selects rows for request.user, while an object permission compares the resolved owner. A list view and a detail view use the same selection policy so a summary path cannot quietly expose a wider collection.

Mechanism and worked example

The test factory deliberately force-authenticates an owned principal.

The test factory deliberately force-authenticates an owned principal. This bypasses credential verification to isolate authorization; the previous token lesson tests that separate boundary. The list returns the selected public rows, and get_object in the detail generic invokes the object check after queryset lookup.

Under the chosen policy, a foreign object is outside the selected queryset and returns 404. That avoids disclosing its existence through this route. A product may deliberately select another disclosure policy, but it should remain consistent and independently tested.

Complete worked example

python

from drf_lab import setup
setup()

from rest_framework import generics, serializers
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.test import APIRequestFactory, force_authenticate
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class Owned(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner_id == request.user.pk
class Scope:
    serializer_class = PublicRole
    permission_classes = [IsAuthenticated, Owned]
    def get_queryset(self):
        return Role.objects.filter(owner=self.request.user).order_by("id")
class RoleList(Scope, generics.ListAPIView):
    pass
class RoleDetail(Scope, generics.RetrieveAPIView):
    pass

def inspect_example(payload):
    alice, bob, _ = reset_fixture()
    if payload.get("deactivate"):
        Role.objects.filter(pk=2).update(active=False)
    request = APIRequestFactory().get("/roles/")
    force_authenticate(request, user=bob if payload.get("user") == "bob" else alice)
    response = RoleDetail.as_view()(request, pk=payload["id"]) if "id" in payload else RoleList.as_view()(request)
    return {"status": response.status_code, "body":
            {"error": response.data["detail"].code} if response.status_code >= 400 else response.data}
Reasoning
The test factory deliberately force-authenticates an owned principal. This bypasses credential verification to isolate authorization; the previous token lesson tests that separate boundary. The list returns the selected public rows, and get_object in the detail generic invokes the object check after queryset lookup. Under the chosen policy, a foreign object is outside the selected queryset and returns 404. That avoids disclosing its existence through this route. A product may deliberately select another disclosure policy, but it should remain consistent and independently tested.

Focused decision: context is in the complete example

python

class Owned(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner_id == request.user.pk

def get_queryset(self):
    return Role.objects.filter(owner=self.request.user).order_by("id")
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample removes the owner queryset predicate.

The counterexample removes the owner queryset predicate. The object permission can still reject some detail reads, but the list now exposes another owner row because that hook is not automatically applied to each list item. A protected detail endpoint is not evidence that the collection is safe.

Do not read an owner name from a query parameter and call it the current user. Identity must come from the authentication boundary. Creation also needs separate authority checks because no stored object exists yet for a detail permission hook to inspect.

Controlled mistake: observe the actual wrong behavior

python

from drf_lab import setup
setup()

from rest_framework import generics, serializers
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.test import APIRequestFactory, force_authenticate
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class Owned(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner_id == request.user.pk
class Scope:
    serializer_class = PublicRole
    permission_classes = [IsAuthenticated, Owned]
    def get_queryset(self):
        return Role.objects.all().order_by("id")
class RoleList(Scope, generics.ListAPIView):
    pass
class RoleDetail(Scope, generics.RetrieveAPIView):
    pass

def inspect_example(payload):
    alice, bob, _ = reset_fixture()
    if payload.get("deactivate"):
        Role.objects.filter(pk=2).update(active=False)
    request = APIRequestFactory().get("/roles/")
    force_authenticate(request, user=bob if payload.get("user") == "bob" else alice)
    response = RoleDetail.as_view()(request, pk=payload["id"]) if "id" in payload else RoleList.as_view()(request)
    return {"status": response.status_code, "body":
            {"error": response.data["detail"].code} if response.status_code >= 400 else response.data}
Reasoning
The counterexample removes the owner queryset predicate. The object permission can still reject some detail reads, but the list now exposes another owner row because that hook is not automatically applied to each list item. A protected detail endpoint is not evidence that the collection is safe. Do not read an owner name from a query parameter and call it the current user. Identity must come from the authentication boundary. Creation also needs separate authority checks because no stored object exists yet for a detail permission hook to inspect.

Independent practice

Hide inactive roles from both list and detail selection while preserving owner scoping.

Hide inactive roles from both list and detail selection while preserving owner scoping. The fixture can mark one owned role inactive before the request. Add the active predicate to the shared get_queryset policy rather than filtering a serialized page afterward.

Check a list with the inactive Israeli role, a detail request for that role and a permitted active bob record. The first should omit it, the second return 404 and the third remain available to its own owner. Do not delete the role to implement visibility.

Step 1: Hide inactive roles consistently in list and detail selection while preserving stored rows and ownership. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from drf_lab import setup
setup()

from rest_framework import generics, serializers
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.test import APIRequestFactory, force_authenticate
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class Owned(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner_id == request.user.pk
class Scope:
    serializer_class = PublicRole
    permission_classes = [IsAuthenticated, Owned]
    def get_queryset(self):
        return Role.objects.filter(owner=self.request.user).order_by("id")
class RoleList(Scope, generics.ListAPIView):
    pass
class RoleDetail(Scope, generics.RetrieveAPIView):
    pass

def inspect_example(payload):
    alice, bob, _ = reset_fixture()
    if payload.get("deactivate"):
        Role.objects.filter(pk=2).update(active=False)
    request = APIRequestFactory().get("/roles/")
    force_authenticate(request, user=bob if payload.get("user") == "bob" else alice)
    response = RoleDetail.as_view()(request, pk=payload["id"]) if "id" in payload else RoleList.as_view()(request)
    return {"status": response.status_code, "body":
            {"error": response.data["detail"].code} if response.status_code >= 400 else response.data}

# Independent task: Hide inactive roles consistently in list and detail selection while preserving stored rows and ownership.
Reasoning
Hide inactive roles consistently in list and detail selection while preserving stored rows and ownership.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution adds active=True to the scoped database query.

The solution adds active=True to the scoped database query. The stored inactive row remains present in the owned fixture, while list and detail views agree about its visibility. Public serializer fields and object permission checks remain unchanged.

A production visibility rule may include teams, roles or retention policy. Centralizing a shared selector can help, but every new action still needs regression coverage. A private response cache must include the same authorization scope; correct database selection can be undermined by a cache shared across users.

Practice solution and acceptance checks

python

from drf_lab import setup
setup()

from rest_framework import generics, serializers
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.test import APIRequestFactory, force_authenticate
from drf_lab import reset_fixture
from drf_lab.models import Role

class PublicRole(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ["id", "title", "country"]
class Owned(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.owner_id == request.user.pk
class Scope:
    serializer_class = PublicRole
    permission_classes = [IsAuthenticated, Owned]
    def get_queryset(self):
        return Role.objects.filter(owner=self.request.user, active=True).order_by("id")
class RoleList(Scope, generics.ListAPIView):
    pass
class RoleDetail(Scope, generics.RetrieveAPIView):
    pass

def inspect_example(payload):
    alice, bob, _ = reset_fixture()
    if payload.get("deactivate"):
        Role.objects.filter(pk=2).update(active=False)
    request = APIRequestFactory().get("/roles/")
    force_authenticate(request, user=bob if payload.get("user") == "bob" else alice)
    response = RoleDetail.as_view()(request, pk=payload["id"]) if "id" in payload else RoleList.as_view()(request)
    return {"status": response.status_code, "body":
            {"error": response.data["detail"].code} if response.status_code >= 400 else response.data}
Reasoning
The solution adds active=True to the scoped database query. The stored inactive row remains present in the owned fixture, while list and detail views agree about its visibility. Public serializer fields and object permission checks remain unchanged. A production visibility rule may include teams, roles or retention policy. Centralizing a shared selector can help, but every new action still needs regression coverage. A private response cache must include the same authorization scope; correct database selection can be undermined by a cache shared across users.

Interview reasoning and next step

Explain why the list counterexample leaks data despite retaining has_object_permission.

Explain why the list counterexample leaks data despite retaining has_object_permission. Then distinguish an inaccessible object from a deleted object and identify the chosen 404 disclosure behavior. Use the two-view test to show consistent policy.

Next, verify session authentication with CSRF. A browser may attach credentials automatically, creating a request-integrity requirement that a token-header fixture does not exercise.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/drf-lab-20261105.zip. Install pinned dependencies in an isolated Python environment. Run verify.py, then change the working exercise baseline and run --practice drf-permissions. The real DRF clients, serializers, schemas and SQLite transactions use owned fixtures only. No production database, feed, account or paid service is used. This lab executes Python; the PC typing trainer and native reading progress do not execute it.

Keep collection visibility, identity and object permissions distinct.