Skip to content

Commit f23bf7d

Browse files
author
Codex
committed
Harden report views and add tenant-isolation tests
1 parent 6b9a08e commit f23bf7d

5 files changed

Lines changed: 115 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
python manage.py seed_requirements || true
3636
python manage.py seed_product_security || true
3737
python manage.py check
38-
python manage.py test apps.core
38+
python manage.py test apps.core apps.reports
3939
4040
nix-devshell:
4141
runs-on: ubuntu-24.04
@@ -56,7 +56,7 @@ jobs:
5656
cp .env.example .env
5757
mkdir -p static media staticfiles
5858
python manage.py check
59-
python manage.py test apps.core
59+
python manage.py test apps.core apps.reports
6060
'
6161
6262
llm-runtime-build:

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ local-check:
1414
. .venv/bin/activate && python manage.py check
1515

1616
local-test:
17-
. .venv/bin/activate && python manage.py test
17+
. .venv/bin/activate && python manage.py test apps.core apps.reports
1818

1919
dev-up:
2020
$(COMPOSE_DEV) up --build

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ make local-check
164164
make local-test
165165
```
166166

167+
`make local-test` deckt aktuell die Basis-Gesundheitschecks und die mandantenbezogenen Report-Views ab.
168+
167169
## Support-Matrix
168170

169171
Siehe [`SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md).

apps/reports/tests.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from django.contrib.auth import get_user_model
2+
from django.test import TestCase
3+
from django.urls import reverse
4+
5+
from apps.organizations.models import Tenant
6+
from apps.reports.models import ReportSnapshot
7+
from apps.wizard.models import AssessmentSession
8+
9+
10+
User = get_user_model()
11+
12+
13+
class ReportViewTests(TestCase):
14+
def setUp(self):
15+
self.tenant_a = Tenant.objects.create(name="Tenant A", slug="tenant-a", country="DE")
16+
self.tenant_b = Tenant.objects.create(name="Tenant B", slug="tenant-b", country="DE")
17+
18+
self.user_a = User.objects.create_user(
19+
username="tenant-a-user",
20+
password="testpass123",
21+
tenant=self.tenant_a,
22+
)
23+
self.user_b = User.objects.create_user(
24+
username="tenant-b-user",
25+
password="testpass123",
26+
tenant=self.tenant_b,
27+
)
28+
29+
self.session_a = AssessmentSession.objects.create(
30+
tenant=self.tenant_a,
31+
started_by=self.user_a,
32+
applicability_result="relevant",
33+
executive_summary="Kurzfassung Tenant A",
34+
)
35+
self.session_b = AssessmentSession.objects.create(
36+
tenant=self.tenant_b,
37+
started_by=self.user_b,
38+
applicability_result="not_relevant",
39+
executive_summary="Kurzfassung Tenant B",
40+
)
41+
42+
self.report_a = ReportSnapshot.objects.create(
43+
tenant=self.tenant_a,
44+
session=self.session_a,
45+
title="Report A",
46+
executive_summary="Executive Summary A",
47+
applicability_result="relevant",
48+
compliance_versions_json={
49+
"ISO27001": {"version": "2022"},
50+
"NIS2": {"version": "2024"},
51+
},
52+
domain_scores_json=[
53+
{"domain": "Governance", "score_percent": 82, "maturity_level": "Managed"}
54+
],
55+
top_measures_json=[{"title": "MFA einführen", "priority": "HIGH"}],
56+
roadmap_summary=[{"name": "Phase 1", "duration_weeks": 6, "objective": "Basis schaffen"}],
57+
next_steps_json={"dependencies": []},
58+
)
59+
self.report_b = ReportSnapshot.objects.create(
60+
tenant=self.tenant_b,
61+
session=self.session_b,
62+
title="Report B",
63+
executive_summary="Executive Summary B",
64+
applicability_result="not_relevant",
65+
)
66+
67+
def test_report_list_only_shows_current_tenant(self):
68+
self.client.force_login(self.user_a)
69+
70+
response = self.client.get(reverse("reports:list"))
71+
72+
self.assertEqual(response.status_code, 200)
73+
reports = list(response.context["reports"])
74+
self.assertEqual(reports, [self.report_a])
75+
76+
def test_report_detail_blocks_foreign_tenant_access(self):
77+
self.client.force_login(self.user_a)
78+
79+
response = self.client.get(reverse("reports:detail", args=[self.report_b.pk]))
80+
81+
self.assertEqual(response.status_code, 404)
82+
83+
def test_report_pdf_is_generated_for_own_tenant(self):
84+
self.client.force_login(self.user_a)
85+
86+
response = self.client.get(reverse("reports:pdf", args=[self.report_a.pk]))
87+
88+
self.assertEqual(response.status_code, 200)
89+
self.assertEqual(response["Content-Type"], "application/pdf")
90+
self.assertTrue(response.content.startswith(b"%PDF"))
91+
92+
def test_report_pdf_blocks_foreign_tenant_access(self):
93+
self.client.force_login(self.user_a)
94+
95+
response = self.client.get(reverse("reports:pdf", args=[self.report_b.pk]))
96+
97+
self.assertEqual(response.status_code, 404)

apps/reports/views.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import io
22
from django.contrib.auth.mixins import LoginRequiredMixin
33
from django.http import HttpResponse
4+
from django.shortcuts import get_object_or_404
45
from django.views.generic import DetailView, ListView, View
56
from apps.core.mixins import TenantAccessMixin
67
from reportlab.lib import colors
@@ -42,7 +43,12 @@ class ReportPdfView(TenantAccessMixin, View):
4243
tenant_filter_field = 'tenant'
4344

4445
def get(self, request, pk):
45-
report = self.filter_queryset_for_tenant(ReportSnapshot.objects.select_related('tenant', 'session')).get(pk=pk)
46+
report = get_object_or_404(
47+
self.filter_queryset_for_tenant(
48+
ReportSnapshot.objects.select_related('tenant', 'session')
49+
),
50+
pk=pk,
51+
)
4652
evidence_needs = RequirementEvidenceNeed.objects.filter(
4753
tenant=report.tenant,
4854
session=report.session,
@@ -135,7 +141,12 @@ class ReportProPdfView(TenantAccessMixin, View):
135141
tenant_filter_field = 'tenant'
136142

137143
def get(self, request, pk):
138-
report = self.filter_queryset_for_tenant(ReportSnapshot.objects.select_related('tenant', 'session')).get(pk=pk)
144+
report = get_object_or_404(
145+
self.filter_queryset_for_tenant(
146+
ReportSnapshot.objects.select_related('tenant', 'session')
147+
),
148+
pk=pk,
149+
)
139150
from .pdf_export import generate_audit_report_pdf
140151
pdf_bytes = generate_audit_report_pdf(report, report.session, report.tenant)
141152
response = HttpResponse(content_type='application/pdf')

0 commit comments

Comments
 (0)