Skip to content

Commit 68a1a9f

Browse files
author
Bart van der Schoor
committed
[#912] Harden access control on case-details/documents
1 parent 0b2e772 commit 68a1a9f

File tree

6 files changed

+233
-18
lines changed

6 files changed

+233
-18
lines changed

src/open_inwoner/accounts/urls.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
path("themes/", MyCategoriesView.as_view(), name="my_themes"),
8888
path("cases/", CasesListView.as_view(), name="my_cases"),
8989
path(
90-
"cases/document/<str:object_id>/",
90+
"cases/<str:object_id>/document/<str:info_id>/",
9191
CasesDocumentDownloadView.as_view(),
9292
name="case_document_download",
9393
),

src/open_inwoner/accounts/views/cases.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from zgw_consumers.concurrent import parallel
1818

1919
from open_inwoner.openzaak.cases import (
20+
fetch_case_roles_for_bsn,
2021
fetch_case_types,
2122
fetch_cases,
2223
fetch_single_case,
@@ -26,6 +27,7 @@
2627
InformatieObject,
2728
create_document_content_stream,
2829
fetch_case_information_objects,
30+
fetch_case_information_objects_for_case_and_info,
2931
fetch_single_information_object,
3032
fetch_single_information_object_uuid,
3133
)
@@ -174,6 +176,10 @@ def get_context_data(self, **kwargs):
174176
case = fetch_single_case(case_uuid)
175177

176178
if case:
179+
# check if we have a role this case
180+
if not fetch_case_roles_for_bsn(case.url, self.request.user.bsn):
181+
raise PermissionDenied()
182+
177183
documents = self.get_case_document_files(case)
178184

179185
statuses = fetch_status_history(case.url)
@@ -243,7 +249,8 @@ def get_case_document_files(self, case) -> List[SimpleFile]:
243249
url=reverse(
244250
"accounts:case_document_download",
245251
kwargs={
246-
"object_id": info_obj.uuid,
252+
"object_id": case.uuid,
253+
"info_id": info_obj.uuid,
247254
},
248255
),
249256
)
@@ -273,12 +280,27 @@ def handle_no_permission(self):
273280
return super().handle_no_permission()
274281

275282
def get(self, *args, **kwargs):
276-
info_object_uuid = kwargs["object_id"]
283+
case_uuid = kwargs["object_id"]
284+
case = fetch_single_case(case_uuid)
285+
if not case:
286+
raise Http404
287+
288+
# check if we have a role this case
289+
if not fetch_case_roles_for_bsn(case.url, self.request.user.bsn):
290+
raise PermissionDenied()
277291

292+
info_object_uuid = kwargs["info_id"]
278293
info_object = fetch_single_information_object_uuid(info_object_uuid)
279294
if not info_object:
280295
raise Http404
281296

297+
# check if this info_object belongs to this case
298+
if not fetch_case_information_objects_for_case_and_info(
299+
case.url, info_object.url
300+
):
301+
raise PermissionDenied()
302+
303+
# check if this info_object should be visible
282304
config = OpenZaakConfig.get_solo()
283305
if not filter_info_object_visibility(
284306
info_object, config.document_max_confidentiality

src/open_inwoner/openzaak/cases.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from zds_client import ClientError
88
from zgw_consumers.api_models.base import factory
99
from zgw_consumers.api_models.catalogi import ZGWModel
10-
from zgw_consumers.api_models.zaken import Zaak
10+
from zgw_consumers.api_models.zaken import Rol, Zaak
1111
from zgw_consumers.service import get_paginated_results
1212

1313
from .clients import build_client
@@ -152,3 +152,31 @@ def fetch_single_case_type(case_type_url: str) -> Optional[ZaakType]:
152152
case_type = factory(ZaakType, response)
153153

154154
return case_type
155+
156+
157+
def fetch_case_roles_for_bsn(case_url: str, bsn: str) -> List[Rol]:
158+
client = build_client("zaak")
159+
160+
if client is None:
161+
return []
162+
163+
try:
164+
response = client.list(
165+
"rol",
166+
request_kwargs={
167+
"params": {
168+
"zaak": case_url,
169+
"betrokkeneIdentificatie__natuurlijkPersoon__inpBsn": bsn,
170+
}
171+
},
172+
)
173+
except RequestException as e:
174+
logger.exception("exception while making request", exc_info=e)
175+
return []
176+
except ClientError as e:
177+
logger.exception("exception while making request", exc_info=e)
178+
return []
179+
180+
roles = factory(Rol, response["results"])
181+
182+
return roles

src/open_inwoner/openzaak/info_objects.py

+30
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,36 @@ def fetch_case_information_objects(case_url: str) -> List[ZaakInformatieObject]:
7979
return case_info_objects
8080

8181

82+
def fetch_case_information_objects_for_case_and_info(
83+
case_url: str, info_object_url: str
84+
) -> List[ZaakInformatieObject]:
85+
client = build_client("zaak")
86+
87+
if client is None:
88+
return []
89+
90+
try:
91+
response = client.list(
92+
"zaakinformatieobject",
93+
request_kwargs={
94+
"params": {
95+
"zaak": case_url,
96+
"informatieobject": info_object_url,
97+
},
98+
},
99+
)
100+
except RequestException as e:
101+
logger.exception("exception while making request", exc_info=e)
102+
return []
103+
except ClientError as e:
104+
logger.exception("exception while making request", exc_info=e)
105+
return []
106+
107+
case_info_objects = factory(ZaakInformatieObject, response)
108+
109+
return case_info_objects
110+
111+
82112
def fetch_single_information_object(info_object_url: str) -> Optional[InformatieObject]:
83113
client = build_client("document")
84114

src/open_inwoner/openzaak/tests/test_documents.py

+105-12
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010
from open_inwoner.accounts.choices import LoginTypeChoices
1111
from open_inwoner.accounts.tests.factories import UserFactory
12+
from open_inwoner.accounts.views.cases import SimpleFile
13+
from open_inwoner.utils.test import paginated_response
1214

13-
from ...accounts.views.cases import SimpleFile
1415
from ..models import OpenZaakConfig
1516
from .factories import ServiceFactory
1617

18+
ZAKEN_ROOT = "https://zaken.nl/api/v1/"
1719
CATALOGI_ROOT = "https://catalogi.nl/api/v1/"
1820
DOCUMENTEN_ROOT = "https://documenten.nl/api/v1/"
1921

@@ -31,6 +33,12 @@ def setUpTestData(self):
3133
login_type=LoginTypeChoices.digid, bsn="900222086", email="[email protected]"
3234
)
3335
self.config = OpenZaakConfig.get_solo()
36+
self.zaak_service = ServiceFactory(api_root=ZAKEN_ROOT, api_type=APITypes.zrc)
37+
self.config.zaak_service = self.zaak_service
38+
self.catalogi_service = ServiceFactory(
39+
api_root=CATALOGI_ROOT, api_type=APITypes.ztc
40+
)
41+
self.config.catalogi_service = self.catalogi_service
3442
self.document_service = ServiceFactory(
3543
api_root=DOCUMENTEN_ROOT, api_type=APITypes.drc
3644
)
@@ -40,6 +48,35 @@ def setUpTestData(self):
4048
)
4149
self.config.save()
4250

51+
self.zaak = generate_oas_component(
52+
"zrc",
53+
"schemas/Zaak",
54+
uuid="d8bbdeb7-770f-4ca9-b1ea-77b4730bf67d",
55+
url=f"{ZAKEN_ROOT}zaken/d8bbdeb7-770f-4ca9-b1ea-77b4730bf67d",
56+
zaaktype=f"{CATALOGI_ROOT}zaaktypen/53340e34-7581-4b04-884f",
57+
identificatie="ZAAK-2022-0000000024",
58+
omschrijving="Zaak naar aanleiding van ingezonden formulier",
59+
startdatum="2022-01-02",
60+
einddatum=None,
61+
status=f"{ZAKEN_ROOT}statussen/3da89990-c7fc-476a-ad13-c9023450083c",
62+
)
63+
self.zaak_informatie_object = generate_oas_component(
64+
"zrc",
65+
"schemas/ZaakInformatieObject",
66+
url=f"{ZAKEN_ROOT}zaakinformatieobjecten/e55153aa-ad2c-4a07-ae75-15add57d6",
67+
informatieobject=f"{DOCUMENTEN_ROOT}enkelvoudiginformatieobjecten/014c38fe-b010-4412-881c-3000032fb812",
68+
zaak=f"{ZAKEN_ROOT}zaken/d8bbdeb7-770f-4ca9-b1ea-77b4730bf67d",
69+
aard_relatie_weergave="some content",
70+
titel="",
71+
beschrijving="",
72+
registratiedatum="2021-01-12",
73+
)
74+
self.role = generate_oas_component(
75+
"zrc",
76+
"schemas/Rol",
77+
url=f"{ZAKEN_ROOT}rollen/f33153aa-ad2c-4a07-ae75-15add5891",
78+
betrokkene_identificatie="foo",
79+
)
4380
self.informatie_object_content = "my document content".encode("utf8")
4481
self.informatie_object = generate_oas_component(
4582
"drc",
@@ -60,13 +97,33 @@ def setUpTestData(self):
6097
url=reverse(
6198
"accounts:case_document_download",
6299
kwargs={
63-
"object_id": self.informatie_object["uuid"],
100+
"object_id": self.zaak["uuid"],
101+
"info_id": self.informatie_object["uuid"],
64102
},
65103
),
66104
)
67105

68-
def _setUpMocks(self, m):
106+
def _setUpOASMocks(self, m):
107+
mock_service_oas_get(m, ZAKEN_ROOT, "zrc")
108+
mock_service_oas_get(m, CATALOGI_ROOT, "ztc")
69109
mock_service_oas_get(m, DOCUMENTEN_ROOT, "drc")
110+
111+
def _setUpAccessMocks(self, m):
112+
# the minimal mocks needed to be able to access the information object
113+
self._setUpOASMocks(m)
114+
m.get(self.zaak["url"], json=self.zaak)
115+
m.get(
116+
f"{ZAKEN_ROOT}rollen?zaak={self.zaak['url']}&betrokkeneIdentificatie__natuurlijkPersoon__inpBsn={self.user.bsn}",
117+
json=paginated_response([self.role]),
118+
)
119+
m.get(
120+
f"{ZAKEN_ROOT}zaakinformatieobjecten?zaak={self.zaak['url']}&informatieobject={self.informatie_object['url']}",
121+
# note the real API doesn't return a paginated_response here
122+
json=[self.zaak_informatie_object],
123+
)
124+
125+
def _setUpMocks(self, m):
126+
self._setUpAccessMocks(m)
70127
m.get(self.informatie_object["url"], json=self.informatie_object)
71128
m.get(self.informatie_object["inhoud"], content=self.informatie_object_content)
72129

@@ -75,7 +132,8 @@ def test_document_content_is_retrieved_when_user_logged_in_via_digid(self, m):
75132
url = reverse(
76133
"accounts:case_document_download",
77134
kwargs={
78-
"object_id": self.informatie_object["uuid"],
135+
"object_id": self.zaak["uuid"],
136+
"info_id": self.informatie_object["uuid"],
79137
},
80138
)
81139
response = self.app.get(url, user=self.user)
@@ -94,7 +152,8 @@ def test_document_content_is_retrieved_when_user_logged_in_via_digid(self, m):
94152
)
95153

96154
def test_document_content_with_bad_status_is_http_403(self, m):
97-
mock_service_oas_get(m, DOCUMENTEN_ROOT, "drc")
155+
self._setUpAccessMocks(m)
156+
98157
info_object = generate_oas_component(
99158
"drc",
100159
"schemas/EnkelvoudigInformatieObject",
@@ -109,13 +168,15 @@ def test_document_content_with_bad_status_is_http_403(self, m):
109168
url = reverse(
110169
"accounts:case_document_download",
111170
kwargs={
112-
"object_id": info_object["uuid"],
171+
"object_id": self.zaak["uuid"],
172+
"info_id": info_object["uuid"],
113173
},
114174
)
115175
self.app.get(url, user=self.user, status=403)
116176

117177
def test_document_content_with_bad_confidentiality_is_http_403(self, m):
118-
mock_service_oas_get(m, DOCUMENTEN_ROOT, "drc")
178+
self._setUpAccessMocks(m)
179+
119180
info_object = generate_oas_component(
120181
"drc",
121182
"schemas/EnkelvoudigInformatieObject",
@@ -130,7 +191,8 @@ def test_document_content_with_bad_confidentiality_is_http_403(self, m):
130191
url = reverse(
131192
"accounts:case_document_download",
132193
kwargs={
133-
"object_id": info_object["uuid"],
194+
"object_id": self.zaak["uuid"],
195+
"info_id": info_object["uuid"],
134196
},
135197
)
136198
self.app.get(url, user=self.user, status=403)
@@ -156,27 +218,58 @@ def test_anonymous_user_has_no_access_to_download_page(self, m):
156218
f"{reverse('login')}?next={self.informatie_object_file.url}",
157219
)
158220

221+
def test_no_data_is_retrieved_when_case_object_http_404(self, m):
222+
self._setUpOASMocks(m)
223+
m.get(self.zaak["url"], status_code=404)
224+
225+
self.app.get(self.informatie_object_file.url, user=self.user, status=404)
226+
227+
def test_no_data_is_retrieved_when_no_related_roles_are_found_for_user_bsn(self, m):
228+
self._setUpOASMocks(m)
229+
m.get(self.zaak["url"], json=self.zaak)
230+
m.get(
231+
f"{ZAKEN_ROOT}rollen?zaak={self.zaak['url']}&betrokkeneIdentificatie__natuurlijkPersoon__inpBsn={self.user.bsn}",
232+
# no roles found
233+
json=paginated_response([]),
234+
)
235+
self.app.get(self.informatie_object_file.url, user=self.user, status=403)
236+
237+
def test_no_data_is_retrieved_when_no_matching_case_info_object_is_found(self, m):
238+
self._setUpOASMocks(m)
239+
m.get(self.zaak["url"], json=self.zaak)
240+
m.get(
241+
f"{ZAKEN_ROOT}rollen?zaak={self.zaak['url']}&betrokkeneIdentificatie__natuurlijkPersoon__inpBsn={self.user.bsn}",
242+
json=paginated_response([self.role]),
243+
)
244+
m.get(self.informatie_object["url"], json=self.informatie_object)
245+
m.get(
246+
f"{ZAKEN_ROOT}zaakinformatieobjecten?zaak={self.zaak['url']}&informatieobject={self.informatie_object['url']}",
247+
# no case info objects found
248+
json=[],
249+
)
250+
self.app.get(self.informatie_object_file.url, user=self.user, status=403)
251+
159252
def test_no_data_is_retrieved_when_info_object_http_404(self, m):
160-
mock_service_oas_get(m, DOCUMENTEN_ROOT, "drc")
253+
self._setUpAccessMocks(m)
161254
m.get(self.informatie_object["url"], status_code=404)
162255

163256
self.app.get(self.informatie_object_file.url, user=self.user, status=404)
164257

165258
def test_no_data_is_retrieved_when_info_object_http_500(self, m):
166-
mock_service_oas_get(m, DOCUMENTEN_ROOT, "drc")
259+
self._setUpAccessMocks(m)
167260
m.get(self.informatie_object["url"], status_code=500)
168261

169262
self.app.get(self.informatie_object_file.url, user=self.user, status=404)
170263

171264
def test_no_data_is_retrieved_when_document_download_data_http_404(self, m):
172-
mock_service_oas_get(m, DOCUMENTEN_ROOT, "drc")
265+
self._setUpAccessMocks(m)
173266
m.get(self.informatie_object["url"], json=self.informatie_object)
174267
m.get(self.informatie_object["inhoud"], status_code=404)
175268

176269
self.app.get(self.informatie_object_file.url, user=self.user, status=404)
177270

178271
def test_no_data_is_retrieved_when_document_download_data_http_500(self, m):
179-
mock_service_oas_get(m, DOCUMENTEN_ROOT, "drc")
272+
self._setUpAccessMocks(m)
180273
m.get(self.informatie_object["url"], json=self.informatie_object)
181274
m.get(self.informatie_object["inhoud"], status_code=500)
182275

0 commit comments

Comments
 (0)