Skip to content

Commit 6628a77

Browse files
authored
Merge pull request #329 from maykinmedia/feature/912-harden-cases-access
[#912] Harden access control on case-details/documents
2 parents 636edf2 + ed398d5 commit 6628a77

File tree

5 files changed

+282
-71
lines changed

5 files changed

+282
-71
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/", CaseListView.as_view(), name="my_cases"),
8989
path(
90-
"cases/document/<str:object_id>/",
90+
"cases/<str:object_id>/document/<str:info_id>/",
9191
CaseDocumentDownloadView.as_view(),
9292
name="case_document_download",
9393
),

src/open_inwoner/accounts/views/cases.py

+72-47
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dataclasses
22
from typing import List
33

4-
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
4+
from django.contrib.auth.mixins import AccessMixin
55
from django.core.cache import cache
66
from django.core.exceptions import PermissionDenied
77
from django.http import Http404, StreamingHttpResponse
@@ -14,9 +14,12 @@
1414

1515
from view_breadcrumbs import BaseBreadcrumbMixin
1616

17+
from open_inwoner.openzaak.api_models import Zaak
1718
from open_inwoner.openzaak.cases import (
1819
fetch_case_information_objects,
20+
fetch_case_information_objects_for_case_and_info,
1921
fetch_cases,
22+
fetch_roles_for_case_and_bsn,
2023
fetch_single_case,
2124
fetch_specific_statuses,
2225
fetch_status_history,
@@ -35,24 +38,52 @@
3538
from open_inwoner.openzaak.utils import filter_info_object_visibility
3639

3740

38-
class CaseListView(
39-
BaseBreadcrumbMixin, LoginRequiredMixin, UserPassesTestMixin, TemplateView
40-
):
41-
template_name = "pages/cases/list.html"
41+
class CaseAccessMixin(AccessMixin):
42+
"""
43+
Shared authorisation check
4244
43-
@cached_property
44-
def crumbs(self):
45-
return [(_("Mijn aanvragen"), reverse("accounts:my_cases"))]
45+
Base checks:
46+
- user is authenticated
47+
- user has a BSN
48+
49+
When retrieving a case :
50+
- users BSN has a role for this case
51+
"""
52+
53+
case: Zaak = None
54+
55+
def dispatch(self, request, *args, **kwargs):
56+
if not request.user.is_authenticated:
57+
return self.handle_no_permission()
58+
59+
if not request.user.bsn:
60+
return self.handle_no_permission()
61+
62+
if "object_id" in kwargs:
63+
case_uuid = kwargs["object_id"]
64+
self.case = fetch_single_case(case_uuid)
65+
66+
if self.case:
67+
# check if we have a role in this case
68+
if not fetch_roles_for_case_and_bsn(self.case.url, request.user.bsn):
69+
return self.handle_no_permission()
4670

47-
def test_func(self):
48-
return self.request.user.bsn is not None
71+
return super().dispatch(request, *args, **kwargs)
4972

5073
def handle_no_permission(self):
5174
if self.request.user.is_authenticated:
5275
return redirect(reverse("root"))
5376

5477
return super().handle_no_permission()
5578

79+
80+
class CaseListView(BaseBreadcrumbMixin, CaseAccessMixin, TemplateView):
81+
template_name = "pages/cases/list.html"
82+
83+
@cached_property
84+
def crumbs(self):
85+
return [(_("Mijn aanvragen"), reverse("accounts:my_cases"))]
86+
5687
def get_context_data(self, **kwargs):
5788
context = super().get_context_data(**kwargs)
5889

@@ -122,9 +153,7 @@ class SimpleFile:
122153
url: str
123154

124155

125-
class CaseDetailView(
126-
BaseBreadcrumbMixin, LoginRequiredMixin, UserPassesTestMixin, TemplateView
127-
):
156+
class CaseDetailView(BaseBreadcrumbMixin, CaseAccessMixin, TemplateView):
128157
template_name = "pages/cases/status.html"
129158

130159
@cached_property
@@ -137,40 +166,30 @@ def crumbs(self):
137166
),
138167
]
139168

140-
def test_func(self):
141-
return self.request.user.bsn is not None
142-
143-
def handle_no_permission(self):
144-
if self.request.user.is_authenticated:
145-
return redirect(reverse("root"))
146-
147-
return super().handle_no_permission()
148-
149169
def get_context_data(self, **kwargs):
150170
context = super().get_context_data(**kwargs)
151171

152-
case_uuid = context["object_id"]
153-
case = fetch_single_case(case_uuid)
154-
155-
if case:
156-
documents = self.get_case_document_files(case)
172+
if self.case:
173+
documents = self.get_case_document_files(self.case)
157174

158-
statuses = fetch_status_history(case.url)
175+
statuses = fetch_status_history(self.case.url)
159176
statuses.sort(key=lambda status: status.datum_status_gezet)
160177

161-
case_type = fetch_single_case_type(case.zaaktype)
162-
status_types = fetch_status_types(case_type=case.zaaktype)
178+
case_type = fetch_single_case_type(self.case.zaaktype)
179+
status_types = fetch_status_types(case_type=self.case.zaaktype)
163180

164181
status_types_mapping = {st.url: st for st in status_types}
165182
for status in statuses:
166183
status_type = status_types_mapping[status.statustype]
167184
status.statustype = status_type
168185

169186
context["case"] = {
170-
"identification": case.identificatie,
171-
"start_date": case.startdatum,
172-
"end_date": (case.einddatum if hasattr(case, "einddatum") else None),
173-
"description": case.omschrijving,
187+
"identification": self.case.identificatie,
188+
"start_date": self.case.startdatum,
189+
"end_date": (
190+
self.case.einddatum if hasattr(self.case, "einddatum") else None
191+
),
192+
"description": self.case.omschrijving,
174193
"type_description": (
175194
case_type.omschrijving if case_type else _("No data available")
176195
),
@@ -222,7 +241,8 @@ def get_case_document_files(self, case) -> List[SimpleFile]:
222241
url=reverse(
223242
"accounts:case_document_download",
224243
kwargs={
225-
"object_id": info_obj.uuid,
244+
"object_id": case.uuid,
245+
"info_id": info_obj.uuid,
226246
},
227247
),
228248
)
@@ -241,29 +261,30 @@ def get_anchors(self, statuses, documents):
241261
return anchors
242262

243263

244-
class CaseDocumentDownloadView(LoginRequiredMixin, UserPassesTestMixin, View):
245-
def test_func(self):
246-
return self.request.user.bsn is not None
247-
248-
def handle_no_permission(self):
249-
if self.request.user.is_authenticated:
250-
return redirect(reverse("root"))
251-
252-
return super().handle_no_permission()
253-
254-
def get(self, *args, **kwargs):
255-
info_object_uuid = kwargs["object_id"]
264+
class CaseDocumentDownloadView(CaseAccessMixin, View):
265+
def get(self, request, *args, **kwargs):
266+
if not self.case:
267+
raise Http404
256268

269+
info_object_uuid = kwargs["info_id"]
257270
info_object = fetch_single_information_object(uuid=info_object_uuid)
258271
if not info_object:
259272
raise Http404
260273

274+
# check if this info_object belongs to this case
275+
if not fetch_case_information_objects_for_case_and_info(
276+
self.case.url, info_object.url
277+
):
278+
raise PermissionDenied()
279+
280+
# check if this info_object should be visible
261281
config = OpenZaakConfig.get_solo()
262282
if not filter_info_object_visibility(
263283
info_object, config.document_max_confidentiality
264284
):
265285
raise PermissionDenied()
266286

287+
# retrieve and stream content
267288
content_stream = download_document(info_object.inhoud)
268289
if not content_stream:
269290
raise Http404
@@ -275,3 +296,7 @@ def get(self, *args, **kwargs):
275296
}
276297
response = StreamingHttpResponse(content_stream, headers=headers)
277298
return response
299+
300+
def handle_no_permission(self):
301+
# plain error and no redirect
302+
raise PermissionDenied()

src/open_inwoner/openzaak/cases.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from requests import RequestException
55
from zds_client import ClientError
66
from zgw_consumers.api_models.base import factory
7-
from zgw_consumers.api_models.zaken import Status, Zaak
7+
from zgw_consumers.api_models.zaken import Rol, Status, Zaak
88
from zgw_consumers.service import get_paginated_results
99

1010
from .api_models import Zaak, ZaakInformatieObject
@@ -119,3 +119,61 @@ def fetch_specific_statuses(status_urls: List[str]) -> List[Status]:
119119
statuses = factory(Status, list(_statuses))
120120

121121
return statuses
122+
123+
124+
def fetch_roles_for_case_and_bsn(case_url: str, bsn: str) -> List[Rol]:
125+
client = build_client("zaak")
126+
127+
if client is None:
128+
return []
129+
130+
try:
131+
response = client.list(
132+
"rol",
133+
request_kwargs={
134+
"params": {
135+
"zaak": case_url,
136+
"betrokkeneIdentificatie__natuurlijkPersoon__inpBsn": bsn,
137+
}
138+
},
139+
)
140+
except RequestException as e:
141+
logger.exception("exception while making request", exc_info=e)
142+
return []
143+
except ClientError as e:
144+
logger.exception("exception while making request", exc_info=e)
145+
return []
146+
147+
roles = factory(Rol, response["results"])
148+
149+
return roles
150+
151+
152+
def fetch_case_information_objects_for_case_and_info(
153+
case_url: str, info_object_url: str
154+
) -> List[ZaakInformatieObject]:
155+
client = build_client("zaak")
156+
157+
if client is None:
158+
return []
159+
160+
try:
161+
response = client.list(
162+
"zaakinformatieobject",
163+
request_kwargs={
164+
"params": {
165+
"zaak": case_url,
166+
"informatieobject": info_object_url,
167+
},
168+
},
169+
)
170+
except RequestException as e:
171+
logger.exception("exception while making request", exc_info=e)
172+
return []
173+
except ClientError as e:
174+
logger.exception("exception while making request", exc_info=e)
175+
return []
176+
177+
case_info_objects = factory(ZaakInformatieObject, response)
178+
179+
return case_info_objects

0 commit comments

Comments
 (0)