feat(zustaendigkeit): ZUST-004 Runde 2 - Kontrollen und Mängel eingeschränkt
CI / backend-tests (push) Successful in 3m34s
CI / frontend-build (push) Successful in 21s

GET /maengel filtert jetzt nach sichtbaren Standorten (Join auf Objekt),
GET /kontrollen/{id} prüft den Standort des zugehörigen Objekts. Gleiches
additives Muster wie Runde 1 (Objekte/Dokumente) - kein Eintrag in
benutzer_standort_zugriff bleibt weiterhin unrestricted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-13 22:44:06 +02:00
1 parent 346d119054
commit 56fae09a6c
5 files changed
+125 -16

No files matched your search

+39
View File
@@ -15555,3 +15555,42 @@ Keine Commits in dieser Session.
- frontend/src/pages/admin/BenutzerSection.tsx | 27 ++++++++++-
---
## 2026-09-13 22:29 – 22:33 (4m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
- 346d119 docs(backlog): PERS-008/LOAD-007 DECISION-REQUIRED-Reste bereinigt
### Geänderte Dateien
- DEVLOG.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/08_loadout.md | 9 +++------
- arbeitskacheln/12_personnel.md | 12 ++++--------
---
## 2026-09-13 22:33 – 22:33 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/08_loadout.md | 9 +++------
- arbeitskacheln/12_personnel.md | 12 ++++--------
---
## 2026-09-13 22:37 – 22:38 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/08_loadout.md | 9 +++------
- arbeitskacheln/12_personnel.md | 12 ++++--------
---
+17 -13
View File
@@ -179,20 +179,24 @@ war die Vorgängerlücke, diese hier ist die zweite gefundene Lücke).
(Regressionstest ohne Zuordnung, Einschränkungstest inkl. Code-Lookup,
Kein-Rollen-Bypass-Test, Dokument-Download/Liste/Versionen-Test,
GET/PUT-CRUD-Test, Rollen-Gate-Test).
- **DoD:** **Erste Runde umgesetzt (2026-09-13), bewusst NICHT alle
Endpunkte auf einmal** (gleiches Vorgehen wie ZUST-003 selbst
- **DoD:** **Zweite Runde umgesetzt (2026-09-13), bewusst weiterhin NICHT
alle Endpunkte auf einmal** (gleiches Vorgehen wie ZUST-003 selbst
vorschreibt: "kein Big-Bang über alle Stellen"). Migration
`0057_benutzer_standort_zugriff.py`. Umgesetzt:
`GET /objekte`, `GET /objekte/{id}`, `GET /objekte/code/{code}`,
`GET /dokumente`, `GET /dokumente/{id}/download`,
`GET /dokumente/{id}/versionen` (schließt damit den ursprünglichen
DOC-006-Befund tatsächlich). **Noch NICHT eingeschränkt (Follow-up bei
Bedarf, nach demselben Muster):** Kontrollen, Mängel, Historie,
Dashboard (Einsatzbereitschaft/Prüftermine), Wartungsaufträge,
Lagerbewegungen/Lagerplätze, Verleih/Übergabeliste/Zählerstand-
Endpunkte aus ASSET-013/009. Wer diese Lücken ausnutzt, sieht weiterhin
standortübergreifend - kein vollständiger Schutz, bis diese Runden
nachgezogen sind.
`0057_benutzer_standort_zugriff.py`. Umgesetzt (Runde 1): `GET /objekte`,
`GET /objekte/{id}`, `GET /objekte/code/{code}`, `GET /dokumente`,
`GET /dokumente/{id}/download`, `GET /dokumente/{id}/versionen`
(schließt damit den ursprünglichen DOC-006-Befund tatsächlich). Umgesetzt
(Runde 2): `GET /maengel` (Standort-Filter über Join auf `Objekt`),
`GET /kontrollen/{id}` (Standort-Prüfung über `Kontrolle.objekt_id`).
**Noch NICHT eingeschränkt (Follow-up bei Bedarf, nach demselben
Muster):** Historie, Dashboard (Einsatzbereitschaft/Prüftermine),
Wartungsaufträge, Lagerbewegungen/Lagerplätze, Verleih/Übergabeliste/
Zählerstand-Endpunkte aus ASSET-013/009. Wer diese Lücken ausnutzt, sieht
weiterhin standortübergreifend - kein vollständiger Schutz, bis diese
Runden nachgezogen sind. Tests:
`test_mangel_liste_standort_eingeschraenkt`,
`test_kontroll_fortschritt_standort_eingeschraenkt` in
`backend/tests/test_zust004_standort_sichtbarkeit.py`.
---
+5 -1
View File
@@ -26,6 +26,7 @@ from app.services.kontrolle import (
fehlende_material_ids,
starte_kontrolle,
)
from app.services.standort_sichtbarkeit import pruefe_standort_sichtbar
router = APIRouter()
@@ -98,9 +99,12 @@ async def starte_neue_kontrolle(
@router.get("/kontrollen/{kontrolle_id}", response_model=KontrolleFortschritt)
async def hole_kontroll_fortschritt(
kontrolle_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
kontrolle_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user)
) -> KontrolleFortschritt:
kontrolle = await _hole_kontrolle_oder_404(db, kontrolle_id)
objekt = await db.get(Objekt, kontrolle.objekt_id)
if objekt is not None:
await pruefe_standort_sichtbar(db, benutzer_id=current_user.id, standort_id=objekt.standort_id)
result = await db.execute(
select(Kontrollposition).where(Kontrollposition.kontrolle_id == kontrolle_id)
)
+7 -2
View File
@@ -13,6 +13,7 @@ from app.models.objekt import Objekt
from app.schemas.mangel import MangelCreate, MangelRead, MangelStatusUpdate
from app.services import historie as historie_service
from app.services.mangel import aendere_status
from app.services.standort_sichtbarkeit import sichtbare_standort_ids
router = APIRouter()
@@ -44,16 +45,20 @@ async def liste_maengel(
objekt_id: int | None = None,
status_filter: MangelStatus | None = None,
db: AsyncSession = Depends(get_db),
_=Depends(_darf_lesen),
current_user=Depends(_darf_lesen),
) -> list[Mangel]:
"""Prompt-Erweiterung (Mangel-Modul): jeder Mitarbeiter+ darf lesen (Mängel
sind für alle im Einsatzalltag relevant), Status ändern bleibt
Materialverantwortlichen+ vorbehalten (Melden vs. Bearbeiten getrennt)."""
Materialverantwortlichen+ vorbehalten (Melden vs. Bearbeiten getrennt).
ZUST-004-Erweiterung: Standort-Sichtbarkeitsgrenze wie bei Objekten."""
stmt = select(Mangel)
if objekt_id is not None:
stmt = stmt.where(Mangel.objekt_id == objekt_id)
if status_filter is not None:
stmt = stmt.where(Mangel.status == status_filter)
sichtbare = await sichtbare_standort_ids(db, current_user.id)
if sichtbare is not None:
stmt = stmt.join(Objekt, Objekt.id == Mangel.objekt_id).where(Objekt.standort_id.in_(sichtbare))
stmt = stmt.order_by(Mangel.gemeldet_am.desc())
result = await db.execute(stmt)
return list(result.scalars().all())
@@ -144,6 +144,63 @@ async def test_standort_zugriff_get_put(client, admin_user, mitarbeiter_user, st
assert zurueckgesetzt.json() == []
@pytest.mark.asyncio
async def test_mangel_liste_standort_eingeschraenkt(
client, admin_user, mitarbeiter_user, standort_factory, objekt_factory
):
standort_a = await standort_factory("Standort A5")
standort_b = await standort_factory("Standort B5")
objekt_a = await objekt_factory(name="Objekt A5", code="ZUST4-A5", standort=standort_a)
objekt_b = await objekt_factory(name="Objekt B5", code="ZUST4-B5", standort=standort_b)
admin_token = await login(client, "admin1")
await client.post(
"/api/v1/maengel",
json={"objekt_id": objekt_a.id, "beschreibung": "Riss im Gurt"},
headers=auth_header(admin_token),
)
await client.post(
"/api/v1/maengel",
json={"objekt_id": objekt_b.id, "beschreibung": "Lampe defekt"},
headers=auth_header(admin_token),
)
await client.put(
f"/api/v1/benutzer/{mitarbeiter_user.id}/standorte",
json={"standort_ids": [standort_a.id]},
headers=auth_header(admin_token),
)
token = await login(client, "mitarbeiter1")
liste = await client.get("/api/v1/maengel", headers=auth_header(token))
objekt_ids = {m["objekt_id"] for m in liste.json()}
assert objekt_a.id in objekt_ids
assert objekt_b.id not in objekt_ids
@pytest.mark.asyncio
async def test_kontroll_fortschritt_standort_eingeschraenkt(
client, admin_user, mitarbeiter_user, standort_factory, objekt_factory
):
standort_a = await standort_factory("Standort A6")
standort_b = await standort_factory("Standort B6")
objekt_b = await objekt_factory(name="Objekt B6", code="ZUST4-B6", standort=standort_b)
admin_token = await login(client, "admin1")
gestartet = await client.post(
f"/api/v1/objekte/{objekt_b.id}/kontrollen", json={"uebernehmen": False}, headers=auth_header(admin_token)
)
kontrolle_id = gestartet.json()["id"]
await client.put(
f"/api/v1/benutzer/{mitarbeiter_user.id}/standorte",
json={"standort_ids": [standort_a.id]},
headers=auth_header(admin_token),
)
token = await login(client, "mitarbeiter1")
response = await client.get(f"/api/v1/kontrollen/{kontrolle_id}", headers=auth_header(token))
assert response.status_code == 403
@pytest.mark.asyncio
async def test_standort_zugriff_setzen_nur_admin(client, mitarbeiter_user, mitarbeiter2_user):
token = await login(client, "mitarbeiter1")