Skip to content

Commit ab11f66

Browse files
Maffoochclaude
andauthored
Anchor location finding reference authorization to the finding's own product (#14871)
* Anchor location finding reference authorization to the finding's own product `get_authorized_location_finding_reference` in `dojo/location/queries.py` was building its membership `OuterRef` paths against `location__products__product_id` / `location__products__product__prod_type_id`. Because a `Location` can be associated with more than one product via `LocationProductReference`, this allowed a user with access to any product that shared the location to read references for findings belonging to other products on the same location. Switched the four `OuterRef` paths to anchor on the finding's actual product (`finding__test__engagement__product[_…]`), so each row resolves authorization against the single product that owns the finding. Renamed the annotation aliases to `finding__…` to match the new path. `V3EndpointStatusCompatibleViewSet.get_queryset` uses the same helper and picks up the change automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Use versioned fixtures in TestLocationFindingReferenceAuthorization The bug being fixed is in the V3-locations code path, so the test must run with V3_FEATURE_LOCATIONS=True. In that mode the legacy `dojo_testdata.json` fixture fails to load because the Endpoint model is deprecated. `@versioned_fixtures` swaps to `dojo_testdata_locations.json` automatically so the suite passes in both V2 and V3 CI variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 716f908 commit ab11f66

2 files changed

Lines changed: 159 additions & 10 deletions

File tree

dojo/location/queries.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,30 +92,34 @@ def get_authorized_location_finding_reference(permission, queryset=None, user=No
9292
return location_finding_reference
9393

9494
roles = get_roles_for_permission(permission)
95+
# Authorization is anchored to the finding's product, not to any product
96+
# that happens to share the location. A finding belongs to exactly one
97+
# product (via test → engagement → product), so the user must have
98+
# access to *that* product to see the reference.
9599
authorized_product_type_roles = Product_Type_Member.objects.filter(
96-
product_type=OuterRef("location__products__product__prod_type_id"),
100+
product_type=OuterRef("finding__test__engagement__product__prod_type_id"),
97101
user=user,
98102
role__in=roles)
99103
authorized_product_roles = Product_Member.objects.filter(
100-
product=OuterRef("location__products__product_id"),
104+
product=OuterRef("finding__test__engagement__product_id"),
101105
user=user,
102106
role__in=roles)
103107
authorized_product_type_groups = Product_Type_Group.objects.filter(
104-
product_type=OuterRef("location__products__product__prod_type_id"),
108+
product_type=OuterRef("finding__test__engagement__product__prod_type_id"),
105109
group__users=user,
106110
role__in=roles)
107111
authorized_product_groups = Product_Group.objects.filter(
108-
product=OuterRef("location__products__product_id"),
112+
product=OuterRef("finding__test__engagement__product_id"),
109113
group__users=user,
110114
role__in=roles)
111115
location_finding_reference = location_finding_reference.annotate(
112-
location__product__prod_type__member=Exists(authorized_product_type_roles),
113-
location__product__member=Exists(authorized_product_roles),
114-
location__product__prod_type__authorized_group=Exists(authorized_product_type_groups),
115-
location__product__authorized_group=Exists(authorized_product_groups))
116+
finding__product__prod_type__member=Exists(authorized_product_type_roles),
117+
finding__product__member=Exists(authorized_product_roles),
118+
finding__product__prod_type__authorized_group=Exists(authorized_product_type_groups),
119+
finding__product__authorized_group=Exists(authorized_product_groups))
116120
return location_finding_reference.filter(
117-
Q(location__product__prod_type__member=True) | Q(location__product__member=True)
118-
| Q(location__product__prod_type__authorized_group=True) | Q(location__product__authorized_group=True))
121+
Q(finding__product__prod_type__member=True) | Q(finding__product__member=True)
122+
| Q(finding__product__prod_type__authorized_group=True) | Q(finding__product__authorized_group=True))
119123

120124

121125
def get_authorized_location_product_reference(permission, queryset=None, user=None):
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from django.utils.timezone import now
2+
3+
from dojo.authorization.roles_permissions import Permissions, Roles
4+
from dojo.location.models import Location, LocationFindingReference, LocationProductReference
5+
from dojo.location.queries import get_authorized_location_finding_reference
6+
from dojo.location.status import FindingLocationStatus, ProductLocationStatus
7+
from dojo.models import (
8+
Engagement,
9+
Finding,
10+
Product,
11+
Product_Member,
12+
Product_Type,
13+
Role,
14+
Test,
15+
Test_Type,
16+
User,
17+
)
18+
from unittests.dojo_test_case import DojoTestCase, versioned_fixtures
19+
20+
21+
@versioned_fixtures
22+
class TestLocationFindingReferenceAuthorization(DojoTestCase):
23+
24+
"""
25+
`get_authorized_location_finding_reference` was anchoring authorization to
26+
Location.products (the set of products associated with the location).
27+
When two products share a location, a Reader on Product A could read
28+
LocationFindingReference rows for findings that belong to Product B.
29+
30+
Authorization must be anchored to the finding's own product
31+
(finding.test.engagement.product), so this test sets up a shared location
32+
and verifies each Reader only sees their product's references.
33+
"""
34+
35+
fixtures = ["dojo_testdata.json"]
36+
37+
@classmethod
38+
def setUpTestData(cls):
39+
prod_type, _ = Product_Type.objects.get_or_create(name="LocFRef PT")
40+
test_type, _ = Test_Type.objects.get_or_create(name="LocFRef Scan")
41+
reader_role = Role.objects.get(id=Roles.Reader)
42+
43+
cls.product_a = Product.objects.create(
44+
name="LocFRef Product A",
45+
description="A",
46+
prod_type=prod_type,
47+
)
48+
cls.product_b = Product.objects.create(
49+
name="LocFRef Product B",
50+
description="B",
51+
prod_type=prod_type,
52+
)
53+
54+
cls.alice = User.objects.create_user(
55+
username="locfref_alice",
56+
password="not-a-real-secret", # noqa: S106 - test fixture user
57+
)
58+
cls.bob = User.objects.create_user(
59+
username="locfref_bob",
60+
password="not-a-real-secret", # noqa: S106 - test fixture user
61+
)
62+
Product_Member.objects.create(user=cls.alice, product=cls.product_a, role=reader_role)
63+
Product_Member.objects.create(user=cls.bob, product=cls.product_b, role=reader_role)
64+
65+
cls.finding_a = cls._make_finding(cls.product_a, test_type, title="Finding A")
66+
cls.finding_b = cls._make_finding(cls.product_b, test_type, title="Finding B")
67+
68+
# Shared location across both products.
69+
cls.shared_location = Location.objects.create(
70+
location_type="URL",
71+
location_value="https://shared.example.com/",
72+
)
73+
LocationProductReference.objects.create(
74+
location=cls.shared_location,
75+
product=cls.product_a,
76+
status=ProductLocationStatus.Active,
77+
)
78+
LocationProductReference.objects.create(
79+
location=cls.shared_location,
80+
product=cls.product_b,
81+
status=ProductLocationStatus.Active,
82+
)
83+
cls.ref_a = LocationFindingReference.objects.create(
84+
location=cls.shared_location,
85+
finding=cls.finding_a,
86+
status=FindingLocationStatus.Active,
87+
)
88+
cls.ref_b = LocationFindingReference.objects.create(
89+
location=cls.shared_location,
90+
finding=cls.finding_b,
91+
status=FindingLocationStatus.Active,
92+
)
93+
94+
@classmethod
95+
def _make_finding(cls, product, test_type, *, title):
96+
engagement = Engagement.objects.create(
97+
name=f"{product.name} Engagement",
98+
product=product,
99+
target_start=now(),
100+
target_end=now(),
101+
)
102+
test = Test.objects.create(
103+
engagement=engagement,
104+
test_type=test_type,
105+
title=f"{product.name} Test",
106+
target_start=now(),
107+
target_end=now(),
108+
)
109+
return Finding.objects.create(
110+
test=test,
111+
title=title,
112+
description=title,
113+
severity="High",
114+
numerical_severity="S0",
115+
active=True,
116+
verified=True,
117+
)
118+
119+
def test_alice_sees_only_product_a_finding_references(self):
120+
results = list(
121+
get_authorized_location_finding_reference(
122+
Permissions.Location_View, user=self.alice,
123+
).filter(location=self.shared_location),
124+
)
125+
result_ids = {ref.id for ref in results}
126+
self.assertEqual(result_ids, {self.ref_a.id})
127+
128+
def test_bob_sees_only_product_b_finding_references(self):
129+
results = list(
130+
get_authorized_location_finding_reference(
131+
Permissions.Location_View, user=self.bob,
132+
).filter(location=self.shared_location),
133+
)
134+
result_ids = {ref.id for ref in results}
135+
self.assertEqual(result_ids, {self.ref_b.id})
136+
137+
def test_superuser_sees_both_finding_references(self):
138+
admin = User.objects.get(username="admin")
139+
results = list(
140+
get_authorized_location_finding_reference(
141+
Permissions.Location_View, user=admin,
142+
).filter(location=self.shared_location),
143+
)
144+
result_ids = {ref.id for ref in results}
145+
self.assertEqual(result_ids, {self.ref_a.id, self.ref_b.id})

0 commit comments

Comments
 (0)