Problem
Wenn in einer Komponente die Selektion gelöscht wird und andere Komponenten eine Transformation auf diesem Komponentenkanal registriert haben, laufen diese Transformationsscripte gesichert, d.h. die Objekt-security wird geprüft.
Die Engine filtert dabei auch die script-Eingaben auf Sichtbarkeit. Darunter ist auch das gelöschte Objekt. Für nicht-admins greift der SecurityConfigurationService#decide als erstes auf den Typ des Objekts zu (isWithoutSecurity -> tType()). Das scheitert mit DeletedObjectAccess.
Die Exception bricht die Verarbeitung des Löschereignisses ab und die Komponenten werden nicht richtig aktualisiert, das gelöschte Objekt wird weiterhin angezeigt, was zu Folgefehlern führt.
Details
DeletedObjectAccess in SecurityConfigurationService#decide nach dem Löschen eines Objekts (nur Nicht-Admins)
Umgebung: TopLogic 8.0.0-alpha11 mit aktiver modellbasierter Sicherheit (SecurityConfigurationService)
Symptom:
- Ein Nicht-Admin löscht in einem Flow-Chart (FlowChartComponent) ein Objekt, hier eine Fügestufe (JoiningStep).
- Danach wird das Chart nicht aktualisiert, und jede weitere Selektion oder Navigation schlägt fehl:
DeletedObjectAccess: This object, the current version of ... JoiningStep:25273@450 ... was already deleted in revision 451. Access revision: 451
- Das Chart bleibt kaputt, auch nach dem Wechsel auf eine andere Fügefolge.
- Mit root tritt der Fehler nicht auf.
Reproduktion:
- Als Nutzer ohne Admin-Rechte ein Flow-Chart öffnen, dessen Selektionskanal von mindestens einer weiteren Komponente über einen TransformingChannel gelesen wird (linking by expression).
- Einen Knoten selektieren und löschen.
Ablauf (Stacktrace):
- Nach dem Commit des Löschens verteilt GlobalModelEventForwarder#synthesizeModelEvents → EventBuilder#notifyListeners → ModelEventAdapter#handleTLObjectDeletions das Lösch-Event.
- FlowChartComponent#receiveModelDeletedEvent entfernt die gelöschten Knoten aus der Selektion: _selectionModel.setSelection(newSelection).
- Der Selection-Listener ruft forwardToChannel → setChannelSelection → Selectable#setSelected auf und schreibt damit synchron, noch während der Event-Verteilung in den Selektionskanal.
- Ein ReceivingChannel leitet weiter an AbstractDerivedComponentChannel#update, das TransformingChannel#transform aufruft → TransformLinkingByExpression → QueryExecutor#execute.
- QueryExecutor#executeWith filtert bei aktiver Sicherheit alle Eingaben des Skripts auf Discover: args.map(SearchExpression::filterSecurity). Unter den Eingaben ist noch das gelöschte Objekt.
- Aus welchem Eingang der Transformation es genau stammt, habe ich nicht ermittelt. Entscheidend ist: Kanalwerte dürfen während der Event-Verteilung veraltete Referenzen enthalten.
- Weiter über filterSecurityTLObject → ModelAccessRights#isDiscoverAllowed → SecurityConfigurationService#isAllowed → decide → isWithoutSecurity(instance) → instance.tType() → DBKnowledgeItem#checkAlive → DeletedObjectAccess.
- EventBuilder protokolliert „Failed to deliver change event to listener: …joiningStepChart…“. Damit bricht die Verteilung des Events an das Chart ab, und das Chart behält seinen veralteten Zustand.
Ursache im Code (SecurityConfigurationService):
- isAllowed(Person, TLObject, BoundCommandGroup) lässt ein Objekt nur ungeprüft durch, wenn es noch nicht committet ist. isCommitted prüft dafür tHandle().getState() == PERSISTENT.
- Laut KnowledgeItem.State behält ein gelöschtes Objekt aber den Zustand PERSISTENT. Ein eigenes Enum für „gelöscht“ gibt es nicht. Ein gelöschtes Objekt läuft also in decide hinein.
- In decide ist der erste Aufruf isWithoutSecurity(instance), und der ruft instance.tType() auf, was für gelöschte Objekte wirft.
- Dasselbe gilt für die Attribut-Variante isAllowed(Person, TLObject, TLStructuredTypePart, BoundCommandGroup).
- Admins sind nicht betroffen, weil isAllowedBypass (BoundChecker/ThreadContext.isAdmin) vor decide zurückkehrt. Ohne aktive Sicherheit (vor TL 8) wurden die Eingaben gar nicht geprüft, deshalb gab es den Fehler früher nicht.
Warum die Anwendung damit nicht umgehen kann:
- Der Fehler entsteht vor dem Anwendungsskript. Er tritt bei der Eingabefilterung in QueryExecutor#executeWith auf. Ein Schutz im TL-Script der Transformation, etwa ein isValid-Check, kommt nie zur Ausführung.
- without-security hilft nicht. Schon isWithoutSecurity(instance) ruft tType() auf dem gelöschten Objekt auf. Selbst wenn man den Typ von der Sicherheit ausnähme, würde er trotzdem werfen.
- Die Prüfung lässt sich nicht pro Skript abschalten. Die Sicherheitsprüfung einzelner Kanal-Transformationen ist nicht konfigurierbar, nur programmatisch (WithSecurityCheck#setUsesSecurity).
- Ablauf und Kanalinhalt kann die Anwendung nicht steuern. Das Setzen der Selektion und die synchrone Neuberechnung der abhängigen Kanäle löst das Framework selbst aus (FlowChartComponent#receiveModelDeletedEvent), mitten in der Event-Verteilung. Die Anwendung kann weder die Reihenfolge der Event-Zustellung noch den Inhalt der Kanäle zu diesem Zeitpunkt beeinflussen.
- Das Problem ist generisch. Jede Komponente, deren TransformingChannel von einer Selektion mit gelöschtem Objekt abhängt, ist betroffen. In unserer Anwendung sind das u. a. testResultTable, searchResultTable und operationSelectionForm. Die Transformationen aus allen Layouts zu entfernen, wäre ein großer Umbau, und neue Layouts würden den Fehler wieder einführen.
- Admin-Rechte sind keine Lösung. Fachanwender und künftige REST-/KI-Zugriffe sollen gerade nicht mit Admin-Rechten laufen.
Lösungsvorschlag (Engine):
- Bevorzugt: In SecurityConfigurationService#isAllowed, in beiden Varianten, vor decide bzw. isWithoutSecurity:
if (!instance.tValid()) {
// A deleted object is not accessible (and must not be touched).
return false;
}
Damit werden gelöschte Objekte aus den Skript-Eingaben und -Ergebnissen gefiltert, statt eine Exception auszulösen. TLObject#tValid() existiert bereits (tHandle().isAlive()).
- Alternativ bzw. zusätzlich: SearchExpression#filterSecurityTLObject lässt ungültige Objekte vor der Prüfung fallen.
- Optional: FlowChartComponent sollte keine gelöschten Objekte mehr in den Selektionskanal weiterreichen, und eine Exception in einem Kanal-Listener sollte nicht die gesamte Event-Zustellung an die Komponente abbrechen.
Stacktrace (Auszug): ERROR EventBuilder - Failed to deliver change event to listener: .../joiningStepChart.layout.xml#Chart com.top_logic.knowledge.service.db2.DeletedObjectAccess: This object, the current version of ImmutableKnowledgeObject[wrapper: AttributedWrapper(type:difa.ffa.joining:JoiningStep, id: JoiningStep:25273@450, ...)], was already deleted in revision 451. Access revision: 451 at DBKnowledgeItem.checkAlive(DBKnowledgeItem.java:956) ... at PersistentObjectImpl.tType(PersistentObjectImpl.java:62) at SecurityConfigurationService.isWithoutSecurity(SecurityConfigurationService.java:684) at SecurityConfigurationService.decide(SecurityConfigurationService.java:1077) at SecurityConfigurationService.isAllowed(SecurityConfigurationService.java:1049) at ModelAccessRights.isDiscoverAllowed(ModelAccessRights.java:311) at SearchExpression.filterSecurityTLObject(SearchExpression.java:384) at SearchExpression.filterSecurity(SearchExpression.java:368) at Args.map(Args.java:97) at QueryExecutor.executeWith(QueryExecutor.java:416) at TransformLinkingByExpression.lambda$new$0(TransformLinkingByExpression.java:67) at TransformingChannel.transform(TransformingChannel.java:80) ... at Selectable.setSelected(Selectable.java:119) at FlowChartComponent.setChannelSelection(FlowChartComponent.java:580) at FlowChartComponent.forwardToChannel(FlowChartComponent.java:551) ... at DefaultMultiSelectionModel.setSelection(DefaultMultiSelectionModel.java:230) at FlowChartComponent.receiveModelDeletedEvent(FlowChartComponent.java:509) at ModelEventAdapter.handleTLObjectDeletions(ModelEventAdapter.java:241) at EventBuilder.notifyListeners(EventBuilder.java:496) at GlobalModelEventForwarder.synthesizeModelEvents(GlobalModelEventForwarder.java:165)
Lösung
Ein gelöschtes Objekt ist für niemanden zugreifbar. SecurityConfigurationService beantwortet jede Zugriffsprüfung auf ein gelöschtes Objekt mit "nicht erlaubt", bevor es den Typ des Objekts liest – unabhängig vom Nutzer, also auch für Administratoren:
- isAllowed(Person, TLObject, BoundCommandGroup) und isAllowed(Person, TLObject, TLStructuredTypePart, BoundCommandGroup) liefern für ein gelöschtes Objekt false.
- isAllowedCreate(Person, TLClass, TLObject) liefert false, wenn der Erzeugungskontext gelöscht ist.
Damit wird ein gelöschtes Objekt aus dem Ergebnis eines gesicherten TL-Scripts gefiltert (Discover-Prüfung), und ein Attributzugriff auf ein gelöschtes Objekt liefert keinen Wert, statt mit DeletedObjectAccess abzubrechen.
Hinweis: Auf dem aktuellen Stand werden die Eingaben eines Scripts nicht mehr gefiltert, nur noch das Ergebnis; der Fehler trat dort weiterhin auf, sobald das gelöschte Objekt im Ergebnis landete oder ein Attribut davon gelesen wurde.