Additiv neben dem bestehenden 4-Rollen-System (mitarbeiter/material-
verantwortlicher/leitungsverantwortlicher/administration bleiben unverändert,
kein Breaking Change): Admin kann jetzt eigene Rollen (z.B. "Materialwart",
"Helfer" aus der Ursprungs-Anforderung) mit frei wählbaren Einzelrechten aus
einem Berechtigungs-Katalog anlegen und Benutzern zuweisen - auch Benutzern
ganz ohne feste RolleTyp-Zuordnung.
Neue Tabellen: berechtigung (Katalog), rolle (custom, admin-anlegbar),
rolle_berechtigung (M:N), benutzer_rolle_zuordnung (M:N, eigene Tabelle statt
Wiederverwendung des ENUM-basierten benutzer_rolle).
require_roles_or_permission() kombiniert beide Systeme: bestehende feste
Rollen ODER eine passende granulare Berechtigung. Auf die vom Nutzer genannten
Beispiel-Endpunkte angewendet: Material anlegen/bearbeiten, Lagerbewegung
durchführen, Mangel melden/lesen/bearbeiten, Prüfung durchführen - weitere
Endpunkte folgen bei Bedarf nach demselben Muster (require_permission()/
require_roles_or_permission() stehen jetzt als Bausteine bereit).
Neue Endpunkte: GET /berechtigungen, CRUD /rollen, PUT/DELETE
/rollen/{id}/berechtigungen/{id}, PUT/DELETE /benutzer/{id}/rollen/{id}.
Frontend: neuer Admin-Tab "Rollen & Rechte" (Rolle anlegen, Rechte togglen,
Benutzer zuweisen/entfernen).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
import pytest
|
|
|
|
from tests.conftest import auth_header, login
|
|
|
|
|
|
async def _benutzer_ohne_feste_rolle(db_session, login_name: str):
|
|
from app.core.security import hash_password
|
|
from app.models.auth import Benutzer
|
|
|
|
benutzer = Benutzer(
|
|
name="Helfer Ohne Feste Rolle",
|
|
login=login_name,
|
|
passwort_hash=hash_password("test-passwort-123"),
|
|
aktiv=True,
|
|
)
|
|
db_session.add(benutzer)
|
|
await db_session.flush()
|
|
return benutzer
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_custom_rolle_gewaehrt_zugriff_ohne_feste_rolle(client, db_session, admin_user):
|
|
"""Kern des granularen Rechte-Systems: ein Benutzer OHNE jede feste
|
|
RolleTyp-Zuordnung darf trotzdem etwas tun, wenn ihm eine custom Rolle mit
|
|
passender Berechtigung zugewiesen wurde (Nutzer-Beispiel: "Materialwart"
|
|
darf Material erstellen)."""
|
|
helfer = await _benutzer_ohne_feste_rolle(db_session, "materialwart1")
|
|
|
|
admin_token = await login(client, "admin1")
|
|
ohne_rolle = await client.post(
|
|
"/api/v1/materialien",
|
|
json={"name": "Verband", "einheit": "Stück", "materialtyp": "standard"},
|
|
headers=auth_header(await login(client, "materialwart1")),
|
|
)
|
|
assert ohne_rolle.status_code == 403
|
|
|
|
rolle = await client.post(
|
|
"/api/v1/rollen", json={"name": "Materialwart"}, headers=auth_header(admin_token)
|
|
)
|
|
rolle_id = rolle.json()["id"]
|
|
|
|
berechtigungen = await client.get("/api/v1/berechtigungen", headers=auth_header(admin_token))
|
|
material_erstellen = next(b for b in berechtigungen.json() if b["schluessel"] == "material.erstellen")
|
|
|
|
await client.put(
|
|
f"/api/v1/rollen/{rolle_id}/berechtigungen/{material_erstellen['id']}", headers=auth_header(admin_token)
|
|
)
|
|
await client.put(f"/api/v1/benutzer/{helfer.id}/rollen/{rolle_id}", headers=auth_header(admin_token))
|
|
|
|
helfer_token = await login(client, "materialwart1")
|
|
mit_rolle = await client.post(
|
|
"/api/v1/materialien",
|
|
json={"name": "Verband", "einheit": "Stück", "materialtyp": "standard"},
|
|
headers=auth_header(helfer_token),
|
|
)
|
|
assert mit_rolle.status_code == 201
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_legacy_admin_funktioniert_weiterhin_ohne_custom_rolle(client, admin_user):
|
|
"""Regressionstest: bestehende feste Rollen funktionieren unverändert,
|
|
das granulare System ist rein additiv."""
|
|
token = await login(client, "admin1")
|
|
response = await client.post(
|
|
"/api/v1/materialien",
|
|
json={"name": "Schere", "einheit": "Stück", "materialtyp": "standard"},
|
|
headers=auth_header(token),
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_nur_admin_darf_rollen_verwalten(client, mitarbeiter_user):
|
|
token = await login(client, "mitarbeiter1")
|
|
response = await client.post("/api/v1/rollen", json={"name": "X"}, headers=auth_header(token))
|
|
assert response.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rolle_ohne_berechtigung_bleibt_wirkungslos(client, db_session, admin_user):
|
|
helfer = await _benutzer_ohne_feste_rolle(db_session, "leerrolle1")
|
|
admin_token = await login(client, "admin1")
|
|
rolle = await client.post("/api/v1/rollen", json={"name": "Leer"}, headers=auth_header(admin_token))
|
|
rolle_id = rolle.json()["id"]
|
|
await client.put(f"/api/v1/benutzer/{helfer.id}/rollen/{rolle_id}", headers=auth_header(admin_token))
|
|
|
|
helfer_token = await login(client, "leerrolle1")
|
|
response = await client.post(
|
|
"/api/v1/materialien",
|
|
json={"name": "X", "einheit": "Stück", "materialtyp": "standard"},
|
|
headers=auth_header(helfer_token),
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_berechtigung_entfernen_wirkt_sofort(client, db_session, admin_user):
|
|
helfer = await _benutzer_ohne_feste_rolle(db_session, "materialwart2")
|
|
admin_token = await login(client, "admin1")
|
|
rolle = await client.post("/api/v1/rollen", json={"name": "Materialwart2"}, headers=auth_header(admin_token))
|
|
rolle_id = rolle.json()["id"]
|
|
berechtigungen = await client.get("/api/v1/berechtigungen", headers=auth_header(admin_token))
|
|
material_erstellen = next(b for b in berechtigungen.json() if b["schluessel"] == "material.erstellen")
|
|
await client.put(
|
|
f"/api/v1/rollen/{rolle_id}/berechtigungen/{material_erstellen['id']}", headers=auth_header(admin_token)
|
|
)
|
|
await client.put(f"/api/v1/benutzer/{helfer.id}/rollen/{rolle_id}", headers=auth_header(admin_token))
|
|
|
|
await client.delete(
|
|
f"/api/v1/rollen/{rolle_id}/berechtigungen/{material_erstellen['id']}", headers=auth_header(admin_token)
|
|
)
|
|
|
|
helfer_token = await login(client, "materialwart2")
|
|
response = await client.post(
|
|
"/api/v1/materialien",
|
|
json={"name": "X", "einheit": "Stück", "materialtyp": "standard"},
|
|
headers=auth_header(helfer_token),
|
|
)
|
|
assert response.status_code == 403
|