Bericht: Benutzer, Spaces und Rechte – Komplexitätsanalyse
Stand: April 2026 – Arbeitsdokument für Vereinfachungsdiskussion
TL;DR
Das aktuelle System hat vier parallele Rollen-Konzepte, die gleichzeitig existieren. Das ist der Hauptgrund für die gefühlte Komplexität. Redundante Models (SpaceManager + Membership, UserInvitation + AccessRequest) und verteilte Berechtigungsprüfungen verstärken das Problem. Eine Konsolidierung würde die Wartbarkeit deutlich verbessern.
Ist-Zustand
Die vier parallelen Rollen-Systeme
┌─────────────────────────────────────────────────────────┐
│ 1. JWT-Rollen │
│ jwt.roles = ['admin', 'member', 'customer'] │
│ → nur im Token, nicht persistiert │
└─────────────────────────────────────────────────────────┘
↓ abgeleitet
┌─────────────────────────────────────────────────────────┐
│ 2. PrilogInstanceRole │
│ OWNER / ADMIN / MANAGER / MEMBER / GUEST │
│ → in PrilogMembership.instanceRole (DB) │
│ → redundant zu JWT-Rollen! │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 3. MembershipRole (pro Space) │
│ OWNER / ADMIN / SPACE_ADMIN / MEMBER / GUEST + Custom│
│ → in Membership.role (String, kein eigenes Model) │
│ → Permissions via RolePermissionPolicy │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 4. PortalRole │
│ owner / support │
│ → separater Namespace, eigene Capability-Checks │
└─────────────────────────────────────────────────────────┘Datenmodell – relevante Models
| Model | Zweck | Pfad |
|---|---|---|
Membership | User↔Space Zugehörigkeit mit Role | schema.prisma:479 |
SpaceManager | Primary/Deputy Owner (redundant!) | schema.prisma:415 |
UserType | Globale Klassifizierung (Lehrer, Schüler...) | |
SpaceUserTypePolicy | Default-Role pro UserType im Space | schema.prisma:459 |
RolePermissionPolicy | Permissions-Liste pro Role | |
UserInvitation | Token-basierte Einladung durch Admin | |
AccessRequest | Selbst-Anfrage mit Genehmigung | |
PrilogMembership | Tenant-weite Instanzrolle | |
UserDirectoryEntry | Zentrale Benutzerdatenbank |
Berechtigungsprüfung – verteilt an 3 Stellen
- Portal/Customer-API –
requirePortalCapability(request, 'manageUsers') - Platform-API –
ensurePermission(scope, 'member:invite', reply) - Web-Client –
useSpaceCan(spaceId, 'member:invite')
Unterschiedliche Namensräume, unterschiedliche Funktionen, unterschiedliche Fehlerausgaben.
Die 8 Komplexitäts-Punkte
🔴 1. Parallele Rollen-Systeme (kritisch)
Beispiel-Verwirrung: Ein Admin im JWT hat manageUsers Capability, aber in einem Space mit role: "MEMBER" nur message:read, message:create, file:upload. Außer die Role ist OWNER oder ADMIN, dann extra Permissions. Aber wenn space.chatEnabled = false, fallen Message-Permissions trotzdem weg. Wenn er zusätzlich portalRole = 'owner' hat, nochmal andere Global-Capabilities.
Speicherorte der Wahrheit:
jwt.roles→ RAMPrilogMembership.instanceRole→ DB (redundant zu JWT)Membership.role→ DBRolePermissionPolicy.permissions→ DB
🔴 2. Implizite vs. explizite Rechte-Definition
Permissions kommen aus zwei Quellen:
- Implizit: Function
getDefaultRolePolicyPermissions(role)inplatform-v1-space-utils.ts - Explizit: DB-Tabelle
RolePermissionPolicy.permissions
Wer ist die Wahrheit? Unklar. Wenn ein Admin die Role nachträglich ändert, wird der Default nicht neu berechnet.
🟡 3. SpaceUserTypePolicy.defaultRole – vermutlich tot
Das Feld existiert, aber es gibt keinen Code der es bei Einladungen/Access-Requests tatsächlich anwendet. Entweder nie implementiert oder nicht mehr verwendet.
🟡 4. SpaceManager und Membership.role sind redundant
SpaceManager.kind = PRIMARY|DEPUTY könnte theoretisch zusätzliche Rechte geben, aber getAuthScope() schaut nur auf Membership.role – nie auf SpaceManager. Desynchronisierungs-Potential: 100%.
🟡 5. UserInvitation + AccessRequest sind fast identisch
Beide Models haben: fullName, birthDate, birthYear, userTypeId, email, requestedSpaceId, requestedRole, message. Beide erzeugen einen User. Einziger Unterschied: Wer initiiert den Workflow.
🟡 6. Berechtigungsprüfung an drei Stellen mit unterschiedlichen Namensräumen
manageUsers (Portal) vs member:invite (Platform) vs useSpaceCan() (Web-Client). Bei einer Logik-Änderung müssen drei Stellen synchron aktualisiert werden.
🟠 7. Custom Rollen ohne Validierung
Ein Typo beim Anlegen einer Custom-Role ("ADMIM" statt "ADMIN") erzeugt einfach eine neue verwaiste Role. Keine Validierung gegen existierende Rollen.
🟠 8. Space-Features als Permission-Filter
getEffectivePermissions() löscht Permissions nachträglich wenn z.B. space.chatEnabled = false. Das vermischt zwei Konzepte: Features (was kann ein Space) und Permissions (was darf ein User).
Vereinfachungs-Vorschläge
Vorschlag 1 – Rollen konsolidieren (höchste Priorität)
Von 4 auf 1 Rollen-System reduzieren:
- Tenant-Ebene:
PrilogMembership.instanceRolewird einzige Quelle (nicht aus JWT ableiten) - Space-Ebene:
Membership.rolebleibt, aber:- System-Rollen (OWNER, ADMIN, SPACE_ADMIN, MEMBER, GUEST) sind fix in einer neuen
RoleDefault-Tabelle - Custom-Rollen in
RolePermissionPolicymitisSystemRole = false
- System-Rollen (OWNER, ADMIN, SPACE_ADMIN, MEMBER, GUEST) sind fix in einer neuen
- Entfernen: JWT-basierte Role-Ableitung,
SpaceUserTypePolicy.defaultRole
Vorschlag 2 – SpaceManager entfernen
SpaceManager ist nur Duplizierung von Membership.role = 'OWNER'. Die Space-Deletion-Logik prüft sowieso nur Membership.role. Einfach löschen.
Vorschlag 3 – Permissions in eine Quelle
Neue Tabelle RoleDefault mit System-Rollen + hardcoded Permissions. RolePermissionPolicy nur noch für Custom-Rollen. Kein Doppel-Check zwischen Function und DB.
Vorschlag 4 – UserInvitation + AccessRequest → UserOnboarding
Ein Model mit workflowType: 'ADMIN_INVITATION' | 'SELF_REQUEST'. Status-Flow: PENDING → APPROVED → CLAIMED.
Vorschlag 5 – Features und Permissions trennen
Features (space.chatEnabled) entscheiden was die UI zeigt. Permissions entscheiden was ein User darf. Nicht mehr Permissions nachträglich wegfiltern.
Vorschlag 6 – SpaceUserTypePolicy.defaultRole endlich nutzen oder löschen
Beim Anlegen eines Memberships: Wenn SpaceUserTypePolicy für den User-Type existiert, nutze die defaultRole. Sonst MEMBER. Oder das Feld komplett entfernen.
Vorschlag 7 – Permissions-Strings standardisieren
Einheitliches Namespacing: instance:view_portal, instance:manage_users, space:update, space:member:invite. Prefix sagt sofort den Scope.
Risiken bei Umstellung
| Komponente | Abhängigkeit | Risiko |
|---|---|---|
| Portal | RolePermissionPolicy Endpunkt | Mittel |
| Web-Client | getAuthScope() + useSpaceCan() | Hoch |
| Invitations-Router | requestedRole Handling | Mittel |
| Admin-Tools | SpaceManager Logik | Hoch |
| Production-Daten | Migration | Höchst |
Empfohlene Migration-Strategie
- Test-Coverage erhöhen – vor jeder Änderung
- Feature-Flag
USE_NEW_ROLE_SYSTEM=true - Dual-Write – alte und neue Tabellen 3 Monate parallel befüllen
- Validierung – tägliche Reports über Mismatches
- Cutover nach 3 Monaten ohne Errors
- Cleanup der alten Tabellen nach 6 Monaten
Priorität
- Kritisch: Vorschlag 1 (Rollen konsolidieren)
- Hoch: Vorschlag 2 (SpaceManager entfernen)
- Mittel: Vorschlag 4 (Invitation + Request zusammenlegen)
- Nice-to-have: Vorschlag 7 (Namespacing)
Fazit
Das System ist funktional, aber hochkomplex durch historisches Wachstum. Jeder einzelne Layer macht für sich Sinn, die Kombination ist aber schwer zu überblicken. Eine strategische Vereinfachung wäre aufwendig, würde aber:
- Die Einarbeitungszeit für neue Entwickler halbieren
- Permission-Bugs deutlich reduzieren
- Das Admin-UI deutlich übersichtlicher machen
- Die Dokumentation einfacher machen