feat(zustaendigkeit): ZUST-004 Runde 2 - Kontrollen und Mängel eingeschränkt
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:
1 parent
346d119054
commit
56fae09a6c
5 files changed
+125
-16
No files matched your search
@@ -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 ++++--------
|
||||
|
||||
---
|
||||
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user