Sammelticket für Nacharbeiten zu #29088.
1. Pfadnavigation stellt Typen statt Referenzen zur Auswahl
PathElementConfig#getAttribute() konfiguriert die Referenz, über die ein Pfadschritt einer Rollenregel navigiert. Da die Property keine eigenen Optionen deklariert, greift die Klassenannotation von TLModelPartRef (@Options(fun = AllClasses.class)): Die GUI öffnet einen Auswahldialog für Typen, eine Referenz ist nicht auswählbar.
Ebenso ohne Optionen: die <part>- und <singleton>-Einträge der Security-Konfiguration (SecurityConfigurationService), während <class> und <module> einen Dialog haben.
2. Unnötige Konfigurationsoptionen bei den Security-Parents
SecurityParentsConfig#getRules() ist als List<RoleRuleConfig> deklariert, obwohl NavigationRulesImporter nur Typ, Vererbungsflag und Pfad liest. Der Editor bietet zusätzlich Rolle, Quellrolle, Quelltyp, Regeltyp und Ressourcenschlüssel an, die stillschweigend ignoriert werden.
3. Typen von der Zugriffskontrolle ausnehmen
Die Konfiguration kann nicht ausdrücken, dass ein Typ gar keiner Zugriffskontrolle unterliegt. Für solche Typen müssen derzeit Grants für alle Rollen konfiguriert werden.
Gewünscht: ein Flag an den klassen- und modulbasierten Einträgen. Auf Objekte eines Typs ohne Zugriffskontrolle darf jeder zugreifen (Objekt, Attributwerte, Anlegen), unabhängig von seinen Rollen. Untertypen erben die Einstellung; ist ein Modul so konfiguriert, gilt das für alle seine Klassen und deren Untertypen.
{{{#!xml <class name="my.module:MyType" without-security="true" /> }}}
Achtung: without-security ist gefährlich – jeder Benutzer darf Objekte eines solchen Typs lesen, ändern, anlegen und löschen, unabhängig von seinen Rollen. Die Option ist nur als Übergangslösung gedacht, bis die Zugriffsrechte des Typs korrekt konfiguriert sind. Ein Typ, den nur der Anwendungscode verwendet, wird mit internal="true" markiert.
4. Definer's Rights im XML-Importer
Eine Import-Definition ist Anwendungskonfiguration, keine Nutzereingabe, und sie bildet ein externes Dokument auf das Modell ab. Ihre Ausdrücke mit den Rechten des importierenden Nutzers auszuwerten beschädigt Daten, statt sie zu schützen: Die Suche nach einem existierenden Objekt, das der Nutzer nicht lesen darf, liefert stillschweigend nichts – der Import legt ein Duplikat an, statt zu verknüpfen. Ein Schreibzugriff auf ein Objekt, das der Nutzer nicht ändern darf, bricht den Import mitten in seiner Transaktion ab. Ob ein Nutzer überhaupt importieren darf, entscheidet das Ausführungsrecht des umgebenden Kommandos.
Ein ModelBinding trägt daher die Security-Einstellung des Imports (AbstractModelBinding#usesSecurity()) und wendet sie auf jeden Ausdruck an, den es kompiliert. Das Flag ist ein pflichtender Konstruktor-Parameter, damit die Entscheidung beim Aufsetzen eines Imports nicht stillschweigend verloren geht. Die importierenden Kommandos übergeben false: XMLImportCommand, InitialProcessSetupService#importBPML und BPMLUpdateCommand#updateCollaboration.
Ausnahme ist die TL-Script-Funktion parseXml: Sie darf kein Bypass werden. ParseXml ist deshalb ein GenericMethodWithSecurity und gibt die Security-Einstellung des aufrufenden Skriptes an das Binding weiter – ein Skript, das mit den Rechten des angemeldeten Nutzers läuft, kann über den Importer nichts lesen oder schreiben, was es nicht auch direkt darf.
CustomLinking und ScopedImportHandler kompilieren keinen eigenen QueryExecutor mehr, sondern werten über das ModelBinding des Imports aus (ImportContext#eval), damit eine Einstellung für alle Ausdrücke einer Import-Definition gilt.
5. Der anonyme Account gehört nicht in die Standardgruppe
Der PersonGroupsInitializer läuft für jedes neu angelegte Konto – auch für den anonymen Account, den der PersonManager beim Start anlegt – und trägt es in die Standardgruppe ein. Über Group#addMember(TLObject) landet dort zusätzlich die Repräsentanten-Gruppe des Kontos.
Die Standardgruppe ist die Gruppe, die alle Konten haben, und trägt damit typischerweise die Rollen eines gewöhnlichen Nutzers. Mit den modellbasierten Zugriffsrechten erbt ein Besucher, der sich nicht angemeldet hat, alles, was diese Rollen gewähren. Der anonyme Account bleibt deshalb aus der Standardgruppe heraus.
Welches Konto das anonyme ist, entscheidet PersonManager#isAnonymous(Person). Die Prüfung vergleicht den konfigurierten Namen und erkennt das Konto damit auch, während es gerade angelegt wird – getAnonymous() findet es zu diesem Zeitpunkt noch nicht. TLContext#isAnonymousUser() und ExternalAuthenticationServlet benutzen dieselbe Prüfung, statt selbst mit getAnonymous() zu vergleichen.
Zusätzlich darf der anonyme Zugang nicht bearbeitet werden: AnonymousAccountDisabled ist eine ExecutabilityRule, die ein Kommando auf dem anonymen Konto deaktiviert. Beide Aspekte sind konfigurierbar, damit die Regel nicht an ein einzelnes Formular gebunden ist – eine Script-Funktion berechnet das Konto aus dem Zielmodell des Kommandos (die Kontenverwaltung arbeitet auf dem Kontakt, nicht auf dem Konto), und die angezeigte Begründung kann die verweigerte Operation benennen. Ohne Meldung erscheint eine allgemeine.
6. Startreihenfolge von Security-Konfiguration und Modell-Service
Die Security-Konfiguration löst die konfigurierten Typ- und Attributnamen gegen das Anwendungsmodell auf, der SecurityConfigurationService hängt also am ModelService. Der Aufbau des Modells greift umgekehrt auf das Modell zu: Beim Anlegen der Modul-Singletons werden die Default-Werte ihrer Attribute gesetzt, und ein default-by-expression, das Objekte anlegt, führt dabei TL-Script aus und fragt nach dem Create-Recht. Diese Prüfung fragte einen Service, der noch startet – eine solche Anwendung startete gar nicht.
ModelAccessRights#uncheckedSecurity(…) führt eine Aufgabe mit einer Implementierung aus, die jeden Zugriff gewährt; der DynamicModelService öffnet ein solches Fenster um seinen Start. Bewusst explizit und nicht als Fallback, der immer dann greift, wenn die Security-Konfiguration fehlt: Ein SecurityConfigurationService, der nicht startet, darf die Anwendung nicht stillschweigend auf „keine Zugriffskontrolle“ herunterfallen lassen. Außerhalb des Fensters scheitert eine fehlende Security-Konfiguration daher weiterhin laut.
Das Fenster liegt in einem ThreadLocal, damit ein parallel bedienter Request seine Prüfungen nicht verliert, und ein verschachteltes Fenster lässt das umgebende offen. Da das Modulsystem seine Services im System-Kontext startet und die reguläre Prüfung diesen ohnehin passieren lässt, ist der gewährte Zugriff dieselbe Entscheidung, die der gestartete Service getroffen hätte.
Das Demo-Modul test.startupSecurity stellt die Situation nach: ein Singleton, dessen Komposition ihren Wert per default-by-expression anlegt. Der Start der Demo-Anwendung ist damit die Probe für diese Startreihenfolge.
7. Der OpenAPI-Client reicht die Security des Aufrufers nicht durch
Eine Methode eines OpenAPI-Clients ist eine TL-Script-Funktion (RPCMethod), und der Aufruf wertet die konfigurierten Ausdrücke des Requests, der berechneten Werte und der Response-Verarbeitung aus. Diese Ausdrücke wendeten immer die Rechte des angemeldeten Nutzers an, unabhängig davon, wie das aufrufende Skript ausgeführt wurde.
Ein Skript mit Definer's Rights baute seinen Request damit gegen die Nutzerrechte zusammen: Ein Attributzugriff auf ein Objekt, das der Nutzer nicht lesen darf, liefert null – der Request ging mit fehlenden Werten hinaus, statt zu scheitern. Dieselbe Lücke hatte parseXml (siehe Abschnitt 4).
RPCMethod ist deshalb ein GenericMethodWithSecurity. Hat der Aufrufer seine Prüfungen abgeschaltet, läuft der ganze Aufruf in einem Fenster ohne Zugriffsprüfung (ModelAccessRights#uncheckedSecurity, siehe Abschnitt 6) – das deckt genau die Prüfungen der konfigurierten Ausdrücke ab, denn jede von ihnen fragt ModelAccessRights.
Die Alternative, eine gesicherte und eine ungesicherte Variante des CallHandler aufzulösen (wie QueryExecutorMethod), wurde verworfen: Die Call-Builder entstehen einmal je Methoden-Spezifikation über den öffentlichen Erweiterungspunkt CallBuilderFactory#createRequestModifier(MethodSpec); die Einstellung müsste durch eine Schnittstelle gereicht werden, die jede Anwendungsimplementierung eines Call-Builders umsetzt.
Zu beachten: canRead(…) und Verwandte melden innerhalb eines solchen Aufrufs einen gewährten Zugriff, während sie die Rechte des Nutzers berichten, wenn nur das Security-Flag eines Ausdrucks abgeschaltet ist – sie fragen ModelAccessRights unabhängig vom Flag.
Migration
Security-Parents. Die Regeln unter <security-parents> sind jetzt Navigationsregeln. Attribute, die dort bisher stillschweigend ignoriert wurden (role, source-role, source-meta-element, type, resource-key), werden als unbekannte Properties abgewiesen und müssen entfernt werden:
{{{#!xml <security-parents> <rule id="..." meta-element="my.module:MyType" inherit="true" role="myRole" <-- entfernen --> > <path> <step attribute="my.module:MyType#owner"/> </path> </rule> </security-parents> }}}
Konfigurationen, die nur id, meta-element, inherit und den Pfad verwenden, bleiben unverändert gültig.
Model-Bindings des Importers. ApplicationModelBinding und TransientModelBinding verlangen die Security-Einstellung des Imports als Konstruktor-Argument. Projektcode, der ein Binding selbst aufsetzt, muss sie angeben – ein Import externer Daten übergibt false:
{{{#!java // vorher: ModelBinding binding = new ApplicationModelBinding(kb, model);
// nachher: ModelBinding binding = new ApplicationModelBinding(kb, model, false); }}}
Anonymer Account in der Standardgruppe. Bestehende Datenbanken enthalten die Mitgliedschaft bereits. Die Migration Ticket_29447_anonymous_not_in_default_group entfernt sie automatisch – den Account und seine Repräsentanten-Gruppe aus jeder Gruppe mit gesetztem defaultGroup-Flag. Manuell ist nichts zu tun. Anwendungen ohne konfigurierte default-group sind nicht betroffen.
OpenAPI-Client-Aufrufe in Skripten mit Definer's Rights. Ein Skript, das ohne Zugriffsprüfung ausgeführt wird (berechnetes Attribut, Wartungsskript, Migration), ruft eine OpenAPI-Client-Methode jetzt ebenfalls ohne Prüfung auf. Requests, die bisher stillschweigend mit fehlenden Werten hinausgingen, enthalten damit die vollständigen Daten. Projekte, die sich auf das bisherige Verhalten verlassen haben, müssen den Zugriff im Skript selbst einschränken.
Typen ohne Zugriffskontrolle. Eine Anwendung, deren Grants nach dem Upgrade auf die modellbasierten Zugriffsrechte (#29088) noch nicht vollständig sind, kann einzelne Typen oder Module übergangsweise mit without-security="true" von der Prüfung ausnehmen. Diese Markierung muss wieder entfernt werden, sobald die Grants konfiguriert sind. Die Coverage-Ansicht der Zugriffskontrolle zeigt die so markierten Typen.