Skip to content

Commit e1191cf

Browse files
committed
Python: ORM: Add tests for inheritance
1 parent 092cfce commit e1191cf

File tree

4 files changed

+258
-2
lines changed

4 files changed

+258
-2
lines changed

python/ql/test/library-tests/frameworks/django-orm/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
The main test files are:
1+
The interesting ORM tests files can be found under `testapp/orm_*.py`. These are set up to be executed by the [testapp/tests.py](testapp/tests.py) file.
2+
3+
List of interesting tests files (that might go out of date if it is forgotten :flushed:):
24

35
- [testapp/orm_tests.py](testapp/orm_tests.py): which tests flow from source to sink
46
- [testapp/orm_security_tests.py](testapp/orm_form_test.py): shows how forms can be used to save Models to the DB
57
- [testapp/orm_security_tests.py](testapp/orm_security_tests.py): which highlights some interesting interactions with security queries
8+
- [testapp/orm_inheritance.py](testapp/orm_inheritance.py): which highlights how inheritance of ORM models works
69

710
## Setup
811

912
```
10-
pip install django pytest pytest-django
13+
pip install django pytest pytest-django django-polymorphic
1114
```
1215

1316
## Run server

python/ql/test/library-tests/frameworks/django-orm/testapp/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from .orm_tests import *
55
from .orm_security_tests import *
66
from .orm_form_test import *
7+
from .orm_inheritance import *
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
from django.db import models
2+
3+
SOURCE = "source"
4+
5+
6+
def SINK(arg):
7+
print(arg)
8+
assert arg == SOURCE
9+
10+
11+
def SINK_F(arg):
12+
print(arg)
13+
assert arg != SOURCE
14+
15+
16+
# ==============================================================================
17+
# Inheritance
18+
#
19+
# If base class defines a field, there can be
20+
# 1. flow when field is assigned on subclass construction to lookup of base class
21+
# 2. no flow from field assignment on subclass A to lookup of sibling subclass B
22+
# 3. no flow from field assignment on base class to lookup of subclass
23+
# ==============================================================================
24+
25+
# ------------------------------------------------------------------------------
26+
# Inheritance with vanilla Django
27+
# ------------------------------------------------------------------------------
28+
29+
class Book(models.Model):
30+
title = models.CharField(max_length=256)
31+
32+
33+
class PhysicalBook(Book):
34+
physical_location = models.CharField(max_length=256)
35+
same_name_different_value = models.CharField(max_length=256)
36+
37+
38+
class EBook(Book):
39+
download_link = models.CharField(max_length=256)
40+
same_name_different_value = models.CharField(max_length=256)
41+
42+
43+
def save_base_book():
44+
return Book.objects.create(
45+
title=SOURCE,
46+
)
47+
48+
49+
def fetch_book(id):
50+
book = Book.objects.get(id=id)
51+
52+
try:
53+
# This sink should have 2 sources, from `save_base_book` and
54+
# `save_physical_book`
55+
SINK(book.title) # $ flow="SOURCE, l:-10 -> book.title"
56+
# The sink assertion will fail for the EBook, which we handle. The title attribute
57+
# of a Book could be tainted, so we want this to be a sink in general.
58+
except AssertionError:
59+
if book.title == "safe ebook":
60+
pass
61+
else:
62+
raise
63+
64+
assert not isinstance(book, PhysicalBook)
65+
assert not isinstance(book, EBook)
66+
67+
try:
68+
SINK_F(book.physical_location)
69+
raise Exception("This field is not available with vanilla Django")
70+
except AttributeError:
71+
pass
72+
73+
74+
def save_physical_book():
75+
return PhysicalBook.objects.create(
76+
title=SOURCE,
77+
physical_location=SOURCE,
78+
same_name_different_value=SOURCE,
79+
)
80+
81+
82+
def fetch_physical_book(id):
83+
book = PhysicalBook.objects.get(id=id)
84+
85+
# This sink should have only 1 sources, from `save_physical_book`
86+
SINK(book.title) # $ flow="SOURCE, l:-10 -> book.title"
87+
SINK(book.physical_location) # $ flow="SOURCE, l:-10 -> book.physical_location"
88+
SINK(book.same_name_different_value) # $ flow="SOURCE, l:-10 -> book.same_name_different_value"
89+
90+
91+
def save_ebook():
92+
return EBook.objects.create(
93+
title="safe ebook",
94+
download_link="safe",
95+
same_name_different_value="safe",
96+
)
97+
98+
99+
def fetch_ebook(id):
100+
book = EBook.objects.get(id=id)
101+
102+
SINK_F(book.title)
103+
SINK_F(book.download_link)
104+
SINK_F(book.same_name_different_value)
105+
106+
107+
# ------------------------------------------------------------------------------
108+
# Inheritance with `django-polymorphic`, which automatically turns lookups on the
109+
# base class into the right subclass
110+
#
111+
# see https://django-polymorphic.readthedocs.io/en/stable/quickstart.html
112+
# ------------------------------------------------------------------------------
113+
114+
from polymorphic.models import PolymorphicModel
115+
116+
117+
class PolyBook(PolymorphicModel):
118+
title = models.CharField(max_length=256)
119+
120+
121+
class PolyPhysicalBook(PolyBook):
122+
physical_location = models.CharField(max_length=256)
123+
same_name_different_value = models.CharField(max_length=256)
124+
125+
126+
class PolyEBook(PolyBook):
127+
download_link = models.CharField(max_length=256)
128+
same_name_different_value = models.CharField(max_length=256)
129+
130+
131+
def poly_save_base_book():
132+
return PolyBook.objects.create(
133+
title=SOURCE
134+
)
135+
136+
137+
def poly_fetch_book(id, test_for_subclass=True):
138+
book = PolyBook.objects.get(id=id)
139+
140+
try:
141+
# This sink should have 2 sources, from `poly_save_base_book` and
142+
# `poly_save_physical_book`
143+
SINK(book.title) # $ MISSING: flow
144+
# The sink assertion will fail for the PolyEBook, which we handle. The title
145+
# attribute of a PolyBook could be tainted, so we want this to be a sink in general.
146+
except AssertionError:
147+
if book.title == "safe ebook":
148+
pass
149+
else:
150+
raise
151+
152+
if test_for_subclass:
153+
assert isinstance(book, PolyPhysicalBook) or isinstance(book, PolyEBook)
154+
155+
if isinstance(book, PolyPhysicalBook):
156+
SINK(book.title) # $ MISSING: flow
157+
SINK(book.physical_location) # $ MISSING: flow
158+
SINK(book.same_name_different_value) # $ MISSING: flow
159+
elif isinstance(book, PolyEBook):
160+
SINK_F(book.title)
161+
SINK_F(book.download_link)
162+
SINK_F(book.same_name_different_value)
163+
164+
165+
def poly_save_physical_book():
166+
return PolyPhysicalBook.objects.create(
167+
title=SOURCE,
168+
physical_location=SOURCE,
169+
same_name_different_value=SOURCE,
170+
)
171+
172+
173+
def poly_fetch_physical_book(id):
174+
book = PolyPhysicalBook.objects.get(id=id)
175+
176+
SINK(book.title) # $ MISSING: flow
177+
SINK(book.physical_location) # $ MISSING: flow
178+
SINK(book.same_name_different_value) # $ MISSING: flow
179+
180+
181+
def poly_save_ebook():
182+
return PolyEBook.objects.create(
183+
title="safe ebook",
184+
download_link="safe",
185+
same_name_different_value="safe",
186+
)
187+
188+
189+
def poly_fetch_ebook(id):
190+
book = PolyEBook.objects.get(id=id)
191+
192+
SINK_F(book.title)
193+
SINK_F(book.download_link)
194+
SINK_F(book.same_name_different_value)

python/ql/test/library-tests/frameworks/django-orm/testapp/tests.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,61 @@ def test_none_all():
8383
assert len(MyModel.objects.all()) == 1
8484
assert len(MyModel.objects.none().all()) == 0
8585
assert len(MyModel.objects.all().none()) == 0
86+
87+
88+
@pytest.mark.django_db
89+
def test_orm_inheritance():
90+
from .orm_inheritance import (save_physical_book, save_ebook, save_base_book,
91+
fetch_book, fetch_physical_book, fetch_ebook,
92+
PhysicalBook, EBook,
93+
)
94+
95+
base = save_base_book()
96+
physical = save_physical_book()
97+
ebook = save_ebook()
98+
99+
fetch_book(base.id)
100+
fetch_book(physical.id)
101+
fetch_book(ebook.id)
102+
103+
fetch_physical_book(physical.id)
104+
fetch_ebook(ebook.id)
105+
106+
try:
107+
fetch_physical_book(base.id)
108+
except PhysicalBook.DoesNotExist:
109+
pass
110+
111+
try:
112+
fetch_ebook(ebook.id)
113+
except EBook.DoesNotExist:
114+
pass
115+
116+
117+
@pytest.mark.django_db
118+
def test_poly_orm_inheritance():
119+
from .orm_inheritance import (poly_save_physical_book, poly_save_ebook, poly_save_base_book,
120+
poly_fetch_book, poly_fetch_physical_book, poly_fetch_ebook,
121+
PolyPhysicalBook, PolyEBook,
122+
)
123+
124+
base = poly_save_base_book()
125+
physical = poly_save_physical_book()
126+
ebook = poly_save_ebook()
127+
128+
poly_fetch_book(base.id, test_for_subclass=False)
129+
poly_fetch_book(physical.id)
130+
poly_fetch_book(ebook.id)
131+
132+
poly_fetch_physical_book(physical.id)
133+
poly_fetch_ebook(ebook.id)
134+
135+
try:
136+
poly_fetch_physical_book(base.id)
137+
except PolyPhysicalBook.DoesNotExist:
138+
pass
139+
140+
try:
141+
poly_fetch_ebook(ebook.id)
142+
except PolyEBook.DoesNotExist:
143+
pass

0 commit comments

Comments
 (0)