major
#29716
Modellbasierte Leseprüfung: Rollen-Lookup kompiliert pro Objekt und Gruppe neue KB-Abfragen, Nicht-Admin wartet 70 s statt 9 s
Beobachtung
Anwendung DiFa ProTiCo (classic UI) mit modellbasierten Zugriffsrechten, 95.041 Objekte vom Typ difa.protico.production:VehicleOrder, master 4c6f6d7e21, PostgreSQL 16. Ladezeiten der Ansichten (Median aus 3 Läufen):
| = Ansicht = | = root = | = Benutzer mit Rolle ProTiCoReader = | = hasRole-Abfragen (Reader) = |
| Tabelle all(`difa.protico.production:VehicleOrder`) | 9,4 s | 69,4 s | 589.966 |
| Delta-Dialog "Stand A" (all(...).filter(...).groupBy(...)) | 2,0 s | 32,7 s | 291.955 |
Von 69,4 s entfallen nur 1,6 s auf die Ausführung in der Datenbank. Der Rest ist Java: Abfragen kompilieren und Logging.
Konfiguration (dokumentierter Weg "Roles held on the security root", docs/faq/access-configuration.md): {{{#!xml <security-parents>
<rule id="lifecycle-secRoot" meta-element="difa.protico.common:LifecycleObject" inherit="true">
<singleton module="SecurityStructure"/>
</rule>
</security-parents> }}} Grants auf Modulebene (Read für ProTiCoReader/ProTiCoWriter). Die Rollen liegen als hasRole-Zeilen auf SecurityStructure#ROOT. Auf den VehicleOrders selbst ist keine Rolle vergeben.
Ursache
- Jede Leseprüfung (SecurityConfigurationService.isAllowed, aus dem Ergebnisfilter des QueryExecutor und aus AccessLike.lookupValue) landet in BoundedRole.getLocalAndGlobalAndGroupRoles.
- Dort fragt addRoles für die Stellvertretergruppe und jede Gruppe des Benutzers die hasRole-Zeilen des Objekts und jedes Rollen-Elternteils ab: (1+G) x (1+P) Abfragen pro Objekt. Hier G = 2, P = 1, also 6,2 Abfragen pro Objekt.
- Jede Abfrage wird mit Objekt und Gruppe als Literalen neu gebaut und kompiliert (roleAssignmentsForContextAndGroup, DBKnowledgeBase.compileSimpleQuery). Nichts davon wird gecacht. Der AccessDecisionCache (#29637) merkt sich nur die Entscheidung pro Objekt, N verschiedene Objekte sind also N echte Prüfungen.
- Jede Kompilierung ruft 7 x Logger.isDebugEnabled auf. com.top_logic.basic.Logger holt dabei jedes Mal einen slf4j-Logger über LoggerFactory.getLogger, und log4j-slf4j2-impl (Log4jLoggerFactory.getContext) läuft dafür den Stack ab. Das gilt für alle Log-Methoden von Logger.
Per Konfiguration nicht zu umgehen: Ein Access-Parent geht nur über Komposition oder eine to-one-Referenz auf ein persistentes Objekt, und Rollenregeln fragen die direkten Zuordnungen trotzdem pro Objekt ab.
Lösung (Prototyp liegt vor)
- Logger: slf4j-Logger einmal pro Namen in einer ConcurrentHashMap halten. Sicher, weil der log4j-Kontext immer vom Classloader von Logger kommt. Rekonfiguration und Level-Änderungen zur Laufzeit wirken weiter auf bestehende Logger (Tests in TestLogger4).
- Neue Klasse RoleAssignmentMemo: pro Interaktion und Gruppe alle hasRole-Zeilen der Gruppe mit einer Abfrage laden und Objekt und Rollen-Eltern daraus beantworten. Gültigkeit wie beim AccessDecisionCache: am InteractionContext, verworfen bei neuer KB-Revision. Rückfall auf den bisherigen Code
- ohne Interaktion,
- bei ungespeicherten Änderungen im Thread,
- für historische Objekte oder Objekte einer anderen KB,
- für eine Person ohne Stellvertretergruppe.
Gruppen mit mehr als 1.000 Zuordnungen werden nicht geladen, sondern pro Objekt abgefragt. Das gemeinsame Rollen-Elternteil wird dabei nur einmal pro Interaktion abgefragt.
Ergebnis mit beiden Änderungen:
| = Ansicht = | = root = | = Reader vorher = | = Reader nachher = | = hasRole-Abfragen (Reader) = |
| Tabelle all(VehicleOrder) | 9,0 s | 69,4 s | 9,4 s | 589.966 -> 31 |
| Delta "Stand A" | 2,0 s | 32,7 s | 2,4 s | 291.955 -> 34 |
Änderung 1 allein: -22 % bzw. -27 %. Verhalten unverändert: gleiche Zeilenzahlen für Reader und root, Reader darf keine Kostenstelle ändern, Writer schon.
Tests
- Neu: TestRoleLookupPerInteraction (tl-element): Rolle über Gruppe auf Rollen-Elternteil, direkte Zuordnung, Zuordnung und Mitgliedschaft im selben Request committet bzw. entfernt, ungespeicherte Zuordnung mit Rollback, Gruppe mit 1.001 Zuordnungen. Jede Entscheidung wird zusätzlich mit dem alten Abfrageweg verglichen.
- Abfragezahl: 2 für 5 Objekte und 2 für 60 Objekte. Ohne den Fix schlägt derselbe Test mit 20 bzw. 240 fehl.
- Neu: zwei Tests in TestLogger4.
- Grün: Security-Suiten in tl-core (63 Tests), tl-element (64), model.search (81).
- Nicht gelaufen: vollständige Modul-Suiten, andere Datenbanken als H2.
Offene Punkte
- Cluster: Wie beim AccessDecisionCache sieht ein Knoten eine Rollenänderung eines anderen Knotens erst nach dem nächsten Refetch.
- Pro Objekt bleibt kleine Restarbeit: ElementAccessManager.getRoles löst Gruppen und Rollen-Eltern erneut auf. In den Zeiten nicht mehr sichtbar.