Typorithm · medium

Object ownership belongs in the lookup

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

Step 1 of 4 · intro card

Object ownership belongs in the lookup

0%

Concept and contract

Check the authenticated principal and the requested object’s owner together before returning private data.

A requester can be authenticated and still have no right to a particular object. An integer identifier in a path is easy to guess, so merely checking login is not an ownership policy.

This lesson gives one fictional role an owner and attempts requests as its owner, another user and a guest. The response policy hides unavailable private objects behind not-found responses rather than revealing their contents.

Mechanism and worked example

The protected view performs its lookup with both the requested primary key and request.user as owner.

The protected view performs its lookup with both the requested primary key and request.user as owner. That combines object identity and permission scope before serialization.

The helper uses force_login intentionally because this lesson isolates authorization, while the preceding lesson tested real password authentication. A guest still follows the login-required response path, and a missing identifier remains unavailable even to the owner of a different record.

Complete worked example

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.contrib.auth import get_user_model
from django.db import models

class Role(models.Model):
    title = models.CharField(max_length=80)
    owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    class Meta:
        app_label = "framework_lab"

install_models(Role)

from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.test import Client
from django.urls import path

@login_required
def detail(request, role_id):
    role = get_object_or_404(Role, pk=role_id, owner=request.user)
    return JsonResponse({"title": role.title})

urlpatterns = [path("roles/<int:role_id>/", detail)]

def object_request(actor, role_id):
    Role.objects.all().delete()
    User = get_user_model()
    User.objects.all().delete()
    owner = User.objects.create_user(username="owner")
    other = User.objects.create_user(username="other")
    Role.objects.create(id=1, title="Private engineer role", owner=owner)
    client = Client()
    if actor != "guest":
        client.force_login(owner if actor == "owner" else other)
    response = client.get(f"/roles/{role_id}/")
    return {"status": response.status_code, "body": response.json() if response.status_code == 200 else None}
Reasoning
The protected view performs its lookup with both the requested primary key and request.user as owner. That combines object identity and permission scope before serialization.

Counterexamples and common mistakes

The counterexample removes the owner predicate while keeping login_required.

The counterexample removes the owner predicate while keeping login_required. An authenticated second user can then read the private role.

Filtering a frontend list or hiding a button does not close that direct-object request path. A not-found policy also does not replace authentication or audit requirements. Choose 403 versus 404 deliberately for an application, and test that the actual data disclosure boundary matches the chosen policy.

Counterexample: a runnable framework mistake

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.contrib.auth import get_user_model
from django.db import models

class Role(models.Model):
    title = models.CharField(max_length=80)
    owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    class Meta:
        app_label = "framework_lab"

install_models(Role)

from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.test import Client
from django.urls import path

@login_required
def detail(request, role_id):
    role = get_object_or_404(Role, pk=role_id)
    return JsonResponse({"title": role.title})

urlpatterns = [path("roles/<int:role_id>/", detail)]

def object_request(actor, role_id):
    Role.objects.all().delete()
    User = get_user_model()
    User.objects.all().delete()
    owner = User.objects.create_user(username="owner")
    other = User.objects.create_user(username="other")
    Role.objects.create(id=1, title="Private engineer role", owner=owner)
    client = Client()
    if actor != "guest":
        client.force_login(owner if actor == "owner" else other)
    response = client.get(f"/roles/{role_id}/")
    return {"status": response.status_code, "body": response.json() if response.status_code == 200 else None}
Reasoning
The counterexample removes the owner predicate while keeping login_required. An authenticated second user can then read the private role.

Independent practice: one bounded framework change

Include the authorized role identifier in a successful JSON response while keeping every rejected response unchanged.

Include the authorized role identifier in a successful JSON response while keeping every rejected response unchanged. Start with the owner’s request, then repeat the second-user and missing-object cases.

Add only the explicitly selected public response field rather than dumping the model’s internal dictionary. This task changes serialization after authorization and must not move the ownership check into a later presentation-only step.

Step 1: Add the authorized role id to the successful response. Step 2: Keep another user, a guest and a missing record unable to receive private data.

Practice starter: solve before reading the solution

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.contrib.auth import get_user_model
from django.db import models

class Role(models.Model):
    title = models.CharField(max_length=80)
    owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    class Meta:
        app_label = "framework_lab"

install_models(Role)

from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.test import Client
from django.urls import path

@login_required
def detail(request, role_id):
    role = get_object_or_404(Role, pk=role_id, owner=request.user)
    return JsonResponse({"title": role.title})

urlpatterns = [path("roles/<int:role_id>/", detail)]

def object_request(actor, role_id):
    # Remove this placeholder, then adapt the retained baseline:
    # Add the authorized role id to the successful response.
    raise NotImplementedError
    Role.objects.all().delete()
    User = get_user_model()
    User.objects.all().delete()
    owner = User.objects.create_user(username="owner")
    other = User.objects.create_user(username="other")
    Role.objects.create(id=1, title="Private engineer role", owner=owner)
    client = Client()
    if actor != "guest":
        client.force_login(owner if actor == "owner" else other)
    response = client.get(f"/roles/{role_id}/")
    return {"status": response.status_code, "body": response.json() if response.status_code == 200 else None}
Reasoning
Add the authorized role id to the successful response. Keep another user, a guest and a missing record unable to receive private data.
  • Hint 1: Select the field explicitly after the owner-scoped lookup.
  • Hint 2: Do not serialize the model’s internal state dictionary.

Solution, tests and trade-offs

The cases observe actual HTTP status and decoded body, so a view cannot pass by returning a safe-looking helper result while exposing data in the real response.

The cases observe actual HTTP status and decoded body, so a view cannot pass by returning a safe-looking helper result while exposing data in the real response. Practice checks that only the owner receives the title and identifier.

The fixture uses disposable accounts and roles only. It does not attempt to access another real account, infer production identifiers or alter the site’s authorization settings.

Practice solution and acceptance checks

python

from bootstrap_lab import django_setup
django_setup(__name__)

from bootstrap_lab import install_models
from django.contrib.auth import get_user_model
from django.db import models

class Role(models.Model):
    title = models.CharField(max_length=80)
    owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    class Meta:
        app_label = "framework_lab"

install_models(Role)

from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.test import Client
from django.urls import path

@login_required
def detail(request, role_id):
    role = get_object_or_404(Role, pk=role_id, owner=request.user)
    return JsonResponse({"id": role.id, "title": role.title})

urlpatterns = [path("roles/<int:role_id>/", detail)]

def object_request(actor, role_id):
    Role.objects.all().delete()
    User = get_user_model()
    User.objects.all().delete()
    owner = User.objects.create_user(username="owner")
    other = User.objects.create_user(username="other")
    Role.objects.create(id=1, title="Private engineer role", owner=owner)
    client = Client()
    if actor != "guest":
        client.force_login(owner if actor == "owner" else other)
    response = client.get(f"/roles/{role_id}/")
    return {"status": response.status_code, "body": response.json() if response.status_code == 200 else None}
Reasoning
The cases observe actual HTTP status and decoded body, so a view cannot pass by returning a safe-looking helper result while exposing data in the real response. Practice checks that only the owner receives the title and identifier.

Interview reasoning and next step

Explain where the trusted principal comes from and where the requested object identity comes from.

Explain where the trusted principal comes from and where the requested object identity comes from. Then show the exact predicate joining those two inputs.

Related endpoints such as update, delete, exports and nested objects need the same policy, not just the detail page. The following CSRF lesson addresses a different browser-request boundary: even a correctly authorized operation must not accept an unintended cross-site state-changing request.

Optional stretch — after the core task works: Apply the same owner predicate to an owned local update and export fixture.

Computer practice: https://applydjinn.com/typorithm/python-frameworks-lab-20261105.zip. Install the pinned dependencies in an isolated environment, run verify.py --track django, then edit the retained exercise baseline and check it with --practice dj-ownership. The lab uses owned in-memory databases, local framework clients and tiny CPU tensors. It never reads the working ApplyDjinn database, contacts production feeds or downloads models.

Check the authenticated principal and the requested object’s owner together before returning private data.