feat(rollen): granulares Rechte-System (Roadmap Phase 6, letztes Modul)
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
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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
|
||||
Reference in New Issue
Block a user