diff --git a/DEVLOG.md b/DEVLOG.md index df236ba..a83967a 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -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 ++++-------- + +--- diff --git a/arbeitskacheln/17_zustaendigkeit.md b/arbeitskacheln/17_zustaendigkeit.md index 4b5e9b3..caddf6d 100644 --- a/arbeitskacheln/17_zustaendigkeit.md +++ b/arbeitskacheln/17_zustaendigkeit.md @@ -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`. --- diff --git a/backend/app/api/v1/endpoints/kontrollen.py b/backend/app/api/v1/endpoints/kontrollen.py index 40964fe..6d524e4 100644 --- a/backend/app/api/v1/endpoints/kontrollen.py +++ b/backend/app/api/v1/endpoints/kontrollen.py @@ -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) ) diff --git a/backend/app/api/v1/endpoints/mangel.py b/backend/app/api/v1/endpoints/mangel.py index 677f27f..ae933c6 100644 --- a/backend/app/api/v1/endpoints/mangel.py +++ b/backend/app/api/v1/endpoints/mangel.py @@ -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()) diff --git a/backend/tests/test_zust004_standort_sichtbarkeit.py b/backend/tests/test_zust004_standort_sichtbarkeit.py index af80604..3078eaf 100644 --- a/backend/tests/test_zust004_standort_sichtbarkeit.py +++ b/backend/tests/test_zust004_standort_sichtbarkeit.py @@ -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")