Skip to content

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

ModelZweckPfad
MembershipUser↔Space Zugehörigkeit mit Roleschema.prisma:479
SpaceManagerPrimary/Deputy Owner (redundant!)schema.prisma:415
UserTypeGlobale Klassifizierung (Lehrer, Schüler...)
SpaceUserTypePolicyDefault-Role pro UserType im Spaceschema.prisma:459
RolePermissionPolicyPermissions-Liste pro Role
UserInvitationToken-basierte Einladung durch Admin
AccessRequestSelbst-Anfrage mit Genehmigung
PrilogMembershipTenant-weite Instanzrolle
UserDirectoryEntryZentrale Benutzerdatenbank

Berechtigungsprüfung – verteilt an 3 Stellen

  1. Portal/Customer-APIrequirePortalCapability(request, 'manageUsers')
  2. Platform-APIensurePermission(scope, 'member:invite', reply)
  3. Web-ClientuseSpaceCan(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 → RAM
  • PrilogMembership.instanceRole → DB (redundant zu JWT)
  • Membership.role → DB
  • RolePermissionPolicy.permissions → DB

🔴 2. Implizite vs. explizite Rechte-Definition

Permissions kommen aus zwei Quellen:

  • Implizit: Function getDefaultRolePolicyPermissions(role) in platform-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.instanceRole wird einzige Quelle (nicht aus JWT ableiten)
  • Space-Ebene: Membership.role bleibt, aber:
    • System-Rollen (OWNER, ADMIN, SPACE_ADMIN, MEMBER, GUEST) sind fix in einer neuen RoleDefault-Tabelle
    • Custom-Rollen in RolePermissionPolicy mit isSystemRole = false
  • 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 + AccessRequestUserOnboarding

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

KomponenteAbhängigkeitRisiko
PortalRolePermissionPolicy EndpunktMittel
Web-ClientgetAuthScope() + useSpaceCan()Hoch
Invitations-RouterrequestedRole HandlingMittel
Admin-ToolsSpaceManager LogikHoch
Production-DatenMigrationHöchst

Empfohlene Migration-Strategie

  1. Test-Coverage erhöhen – vor jeder Änderung
  2. Feature-Flag USE_NEW_ROLE_SYSTEM=true
  3. Dual-Write – alte und neue Tabellen 3 Monate parallel befüllen
  4. Validierung – tägliche Reports über Mismatches
  5. Cutover nach 3 Monaten ohne Errors
  6. Cleanup der alten Tabellen nach 6 Monaten

Priorität

  1. Kritisch: Vorschlag 1 (Rollen konsolidieren)
  2. Hoch: Vorschlag 2 (SpaceManager entfernen)
  3. Mittel: Vorschlag 4 (Invitation + Request zusammenlegen)
  4. 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