# Technik
Genau! Nun möchten wir über unseren Teil zur Technik reden. Dabei gehen wir auf unser Architektur-Diagramm ein, stellen dabei unseren Tech-Stack vor, sprechen über einige Design-Entscheidungen, die wir getroffen haben und reflektieren schließlich über die gewählten Technologien.
## Tech-Stack & Architektur
Unsere Kern-Applikation besteht aus einem NestJS-Backend, einem VueJS-Frontend und zusätzlich einem Microservice zur PDF-Generierung. Diese Services laufen alle jeweils in einem Docker Container, den wir über ein Kubernetes Cluster deployen und orchestrieren. Außerdem gibt es eine Postgresql-Datenbank und Keycloak fürs Identity- und Access-Management.
Dieser Tech-Stack war von Leo vorgegeben, damit er uns während der Entwicklung bei Fragen unterstützen kann und er das Projekt nach dem Ende selber warten kann. Auf unsere Erfahrungen mit den Technologien werde ich am Ende dieses Teils noch einmal kurz eingehen.
Zudem gibt es noch einen PDF-Generator Microservice, welchen wir ebenfalls mit NestJS aufgesetzt haben. Dieser Service kann nur vom Backend aus erreicht werden und alle von ihm ausgehenden Requests werden von Kubernetes blockiert. Das hängt mit Sicherheitsproblemen der PDF-Generierung zusammen, worauf wir später noch einmal eingehen werden.
KLICK
Wie ihr vielleicht bemerkt habt, haben wir bei all diesem Komponenten auf Docker zurückgegriffen. Zum einen natürlich wegen der generellen Vorteile von Docker, wie erhöhte Portabilität und effektive Ressourcennutzung, aber wir lassen diesen Hauptkern auch innerhalb einer Kubernetes-Instanz laufen. Durch Kubernetes haben wir unter anderem Load-Balancing und die Möglichkeit Rolling-Updates und Deployment-Previews durchzuführen.
KLICK
Kubernetes ist derzeit so aufgesetzt dass von unserer Kern-Applikation zwei Instanzen gestartet werden. Über einen Ingress-Controller kann der Nutzer nun mit unserer Applikation sprechen und wird automatisch an die passende Instanz weitergeleitet.
## Aufgabenerstellung und PDF-Generierung | Ablauf
Wir möchten uns nun mit dem Ablauf der Aufgabenerstellung sowie der PDF-Generierung eines Arbeitsblattes beschäftigen. Unsere Applikation deckt natürlich viele verschiedene Use-Cases und Funktionen ab, aber der Grund warum wir genau über diese sprechen möchten ist, dass sie die Kernfunktionen sind und wir viele größere Diskussionen über den genauen Vorgang und die technische Umsetzung hatten. Bevor wir allerdings auf diese Diskussionen und Entscheidungen zu sprechen kommen, möchten wir nun den Ablauf vorstellen.
## Aufgabenerstellung | Ablauf
Schauen wir uns zunächst die Aufgabenerstellung an.
Wie gesagt beginnt der Prozess damit, dass ein Nutzer auf den entsprechenden Knopf im Frontend drückt. Dies löst im Hintergrund nun einen asynchronen Aufruf an unser Backend aus. Dabei wird über einen POST-Reqest die zu erstellende Aufgabe übergeben und vom Backend in der Datenbank gespeichert.
Das klingt zwar sehr simpel, aber dennoch gab es dabei einiges an Diskussionsbedarf, insbesondere bei der Entscheidung, wie Nutzer den Inhalt eingeben. Wir wussten, dass wir schließlich den Inhalt in ein Format bringen müssen, dass wir in PDF umwandeln können. Dabei gibt es allerdings verschiedene Möglichkeiten. Entweder man erzeugt dieses PDF-fähige Format bereits auf Client-Seite oder erst später bei der PDF-Generierung.
## Aufgabenerstellung | Richtext-Editor
Unsere initiale Idee war es, einen Richtext-Editor zur Erstellung von Aufgaben zu nutzen. Es würde nur wenig Arbeitszeit kosten einen Richtext-Editor zu integrieren, da viele große Bibliotheken dafür verfügbar sind und eine einfache Einbidung ermöglichen. Darüber hinaus arbeiten diese Editoren nach dem "What You See Is What You Get"-Prinzip, was zu einer guten User-Experience führt, da Nutzer direkt sehen, wie ihre Aufgabe am Ende auch auf der generierten PDF aussieht.
Super! Damit sind alle Probleme gelöst und wir sind mit allem fertig. Das dachten wir für wenige Sekunden, bis jemand das Todschlag-Argument der "Mathe-Pyramide" in die Diskussion eingeworfen hatte. Es handelt sich dabei um eine der komplexeren Aufgabenkonstrukte, die wir unterstützten wollten. Alle Vorschläge, wie man eine Mathe-Pyramide gut in diese Editoren integrieren könnte, verliefen im nichts. Mit keiner Lösung waren wir zufrieden und schließlich mussten wir feststellen, dass wir langfristig nur Probleme mit diesen Editoren bekommen werden. Der Hauptgrund dafür ist deren eingeschränkte Flexibilität. Zwar können wir eigene Knöpfe hinzufügen und den Editor beliebig erweitern, aber wir sind lediglich auf das HTML-Format beschränkt und das was wir innerhalb dessen machen können.
Darüber hinaus hat uns Leo darauf hingewiesen, dass diese Editoren sehr schnell zu unsauberem HTML führen, z.B. wenn man Inhalt von anderen Webseiten versucht zu Copy&Pasten.
Entscheidend ist an diesem Punkt anzumerken, dass wir uns durch einen Richtext-Editor endgültig auf das Format HTML festlegen würden. Im vorherigen Teil hatten wir kurz erwähnt, dass wir durch unser flexibles Datenbankformat beliebig wechseln können zwischen HTML, LaTeX und anderen Formaten. Wenn wir allerdings den Inhalt bereits als HTML in der Datenbank festankern, ist dies nicht mehr möglich.
## Aufgabenerstellung | Templates
So sind wir schließlich auf die Idee gekommen, lediglich den semantischen Inhalt der Aufgaben abzuspeichern. Wie im Bild oben rechts zu erkennen ist, haben wir uns dabei für JSON als Wrapper um unser eigenes Format entschieden. Man kann sich das ganze nun so vorstellen, dass wir zunächst mittels dem Property "type" beschreiben, was wir speichern möchten und dann mit dem "content" was der Inhalt ist.
Der Fokus liegt hier also wirklich auf dem "was" und nicht auf dem "wie". Wir haben versucht die Kopplung zwischen Inhalt und Format möglichst stark aufzuheben. Dies ermöglicht uns eine deutlich stärkere Kontrolle über die Struktur der gespeicherten Aufgaben und flexibel zwischen verschiedenen Formaten zu wechseln.
Der größte Nachteil ist allerdings, dass dieser Eigenbau mit einem massiven Aufwand verbunden ist. Wir können hier nicht einfach die Bibliothek eines großen Open-Source-Projekts einbinden und sind fertig. Wir müssen uns sowohl Format als auch den Eingabe-Editor überlegen und entwickeln. Darüber hinaus müssen wir im Backend ebenfalls eine Umwandlung des eigenen Formats in ein Format, das zur PDF-Generierung genutzt werden kann, durchführen.
Da die Vorteile die Nachteile insgesamt allerdings überwiegen, haben wir uns – wie ihr vielleicht schon raushört – für diese Lösung entschieden.
## PDF-Generierung | Ablauf
Nun betrachten wir den zweiten Teil dieses Sequenzdiagramms. Dabei gehen wir davon aus, dass der Nutzer nun einige Aufgaben erstellt, zu einem Arbeitsblatt hinzugefügt hat und sich nun das passende PDF dazu generieren möchte.
Zunächst klickt der Nutzer ähnlich wie im ersten Teil auf den entsprechenden Knopf im Frontend.
Unser Backend empfängt diese Anfrage auf Controller-Ebene und startet auf Service- bzw. Business-Logik-Ebene nun die eigentliche Generierung. Da für die Generierung natürlich der Inhalt und weitere Meta-Daten des Arbeitsblattes benötigt werden, holt sich das Backend die entsprechenden Daten aus der Datenbank. Von dort erhält das Backend die Daten allerdings noch in dem semantischen Format, in dem sie zur Generierung unbrauchbar sind. Wir haben uns dazu entschieden, die Arbeitsblätter in HTML für die Generierung umzuwandeln.
Sobald das endgültige HTML generiert worden ist, kann es an unseren PDF-Microservice geschickt werden. Mittels einer Headless Chrome-Browser Instanz, in unserem Fall von Puppeteer, wird nun das HTML in PDF umgewandelt und anschließend wieder an unser Backend gesendet.
****Der Backend-Service hat nun die vom Nutzer angefragte Ressource vorliegen und kann sie zurück an den Controller reichen. Der Controller muss schließlich nur noch entsprechende Header für die Response setzen, damit das Frontend diese korrekt empfangen kann und kann die Anfrage dann abschließen. Das Frontend erhält das PDF und kann es dem Nutzer präsentieren.****
## Arbeitsblatt-Generierung | Content-Format
Wir haben gerade erwähnt, dass wir die in der Datenbank gespeicherten Daten vor der PDF-Generierung in HTML umwandeln.
Wenn wir über die Formate der Arbeitsblatt-Generierung gesprochen und diskutiert haben, stand dabei vor allem immer das Eingabe- und Speicherungsformat im Vordergrund. Das Ausgabeformat stand im Endeffekt seit Tag 1 fest. PDF bietet viele Vorteile, wie bspw. dessen Portabilität, einfache Druckbarkeit, Unmengen an nützlichen Libraries, Generierungs-Tools und schließlich war PDF auch einfach unser Kundenwunsch.
Die Entscheidung über das Eingabeformat ist uns leider signifikant schwerer gefallen. Hauptsächlich standen dabei HTML und LaTeX zur Debatte.
Die beiden großen Vorteile an HTML sind, dass es wirklich sehr leicht zu erstellen ist und wir bereits nach kurzer Zeit vielversprechende Ergebnisse erzielen könnten. Darüber hinaus könnten wir den HTML-Inhalt ebenfalls direkt dem Nutzer als Live-Preview anzeigen, da Browser dies nativ verstehen würden.
Das führt allerdings zu vielen Sicherheitsproblemen insbesondere auf der Serverseite. Wir müssen sehr genau überprüfen was Nutzer an unseren Server übermitteln, die Eingaben bereinigen, darüber hinaus noch den Headless Browser von unserem Backend entkoppeln und somit unser Netzwerk absichern.
Darüber hinaus ist HTML leider in seiner Funktionalität sehr beschränkt. Wir haben uns im Projekt natürlich mit der "Legacy-Arbeitsblatt-Generierung" beschäftigt, welche aus Libre-Office und Kopiererei bestand. Viele Dinge, die auf diese Weise erstellt worden sind, waren mit HTML zwar leicht umsetzbar, andere hingegen nur mit enormen Aufwand und sehr "hacky" Lösungen. Ein Beispiel was in Diskussionen um HTML jedes Mal gefallen ist, sind die altbekannten Mathe-Pyramiden.
Auf der Suche, wie man diese mit HTML umsetzen könnte, sind wir mehrfach auf LaTeX gestoßen und haben gesehen wie leicht dies mit LaTeX umsetzbar wäre. Gerade wenn es darum geht komplexe Strukturen umzusetzen würden wir sehr von LaTeX profitieren und einfache Strukturen sind ja ohnehin kein Problem.
Diese Vorteile sind allerdings mit einem höheren initialen Aufwand verbunden und vom Entwickler werden nun neben HTML auch noch LaTeX-Kenntnisse abgefordert.
Dass wir mi LaTeX keine Chrome Sandbox benötigen würden ist ein Pro in der Debatte um Sicherheit, allerdings müssen wir ehrlich gestehen, dass es bei LaTeX garantiert andere Sicherheitsrisiken gibt. Mit diesen haben wir uns nur nicht so genau außeinandergesetzt, da wir uns am Ende auf die HTML-Variante fokussiert haben.
Vor allem aus dem Grund, dass wir erst sehr spät im Kurs mit der Arbeitsblatt-Generierung begonnen haben, haben wir uns schließlich dazu entschieden HTML zu verwenden. Um allerdings nicht endgültig auf diesem Format fixiert zu sein, verwenden wir in der Datenbank ein Speicherungsformat, was unabhängig von konkreter Syntax ist und sich einzig und allein auf die Semantik beschränkt. Dies erlaubt uns auch problemlos auf LaTeX umzustellen oder eine hybride Variante anzustreben, sollte es notwendig werden.
## Arbeitsblatt-Generierung | Sicherheitshürden
Wie wir schon vorhin angesprochen hatten, gab es bei der PDF-Generierung der Arbeitsblätter einige Sicherheitsaspekte, auf die wir erst nach der Implementierung aufmerksam wurden.
Wir haben hier zwei Beispiele mitgebracht, die potenzielle Exploits unserer PDF-Generierung zeigen und was für entsprechende Maßnahmen wir dagegen ergriffen haben.
Die erste Sicherheitshürde, die wir umgehen mussten, war eine typische HTML-Injection. Mit dieser Technik konnte man beliebiges HTML in den Titel oder Body des Arbeitsblattes schreiben. Dies wird vom Backend in das PDF Template hereingegeben. Hiermit war es zum Beispiel möglich, Bilder, Bold-Text oder andere ungewünschte HTML-Tags in das Arbeitsblatt zu schreiben. Das sind noch relativ harmlose Beispiele, wenn man sich überlegt, dass darüber auch ein Script-Tag injiziert werden kann, über das man JavaScript-Code ausführen konnte. Der Höhepunkt war damit erreicht, dass wir Puppeteer ohne eine Browser-Sandbox gestartet haben, wodurch man sogar auf den Server zugreifen konnte. Glücklicherweise konnten wir dies mit relativ wenig Umstand durch eine sanitization-library umgehen, welche sämtliche HTML tags bzw. Symbole aus dem Content entfernt.
Der zweite Sicherheitsaspekt, welcher ebenfalls bei der Generierung aufgetreten ist, war eine mögliche Server-Side-Request-Forgery-Attacke. Dabei war es möglich über den gespeicherten Inhalt auf interne Ressourcen des Clusters zuzugreifen. Dies wurde uns damit demonstriert, dass man plötzlich auf Bilder, die innerhalb des internen Bereichs liegen, zugreifen konnte. Das haben wir gelöst, indem wir die PDF-Generierung in einen eigenen Microservice ausgelagert haben, der nur sehr beschränkt im Netzwerk kommunizieren kann.
Vor allem haben wir zum Thema Sicherheit gelernt, dass man sich eigentlich immer auf das schlimmste einstellen sollte, wenn man mit user-generiertem Content arbeitet, da es nur eine Frage der Zeit ist, bis jemand eine Sicherheitslücke entdeckt.
## Reflexion
Wir möchten abschließend reflektieren und darüber sprechen, was aus technischer Sicht gut lief und was verbesserungswürdig gewesen wäre.
Grundsätzlich sind wir mit der Wahl der Frameworks, der Architektur und den meisten Entwicklungsentscheidungen sehr zufrieden. Einige Dinge sind uns allerdings aufgefallen, die wir in einem zukünftigen Projekt wahrscheinlich anders machen würden.
Als ersten Punkt geht es dabei um Sicherheit. Im Großteil des Projekts haben wir uns mit diesem Thema überhaupt nicht auseinandergesetzt. Erst als dann Sicherheitsprobleme auftraten - und Leo uns glücklicherweise darauf aufmerksam gemacht hat - haben wir uns damit beschäftigt. Da es nun um schon fertig implementierte Features ging, in denen bei uns Sicherheitslücken auftraten, mussten wir uns ein zweites Mal mit diesen befassen. Als Lehre haben wir deshalb daraus gezogen, konsequent von Anfang des Projekts bei jeder funktionalen Erweiterung den Sicherheitsaspekt mit zu betrachten. So hätten wir den Aufwand für Features im Vorhinein besser einschätzen, und - viel wichtiger - Sicherheitslücken vorbeugen können. Bei der Entwicklung von IDEA haben wir dadurch unerwarteten Aufwand im Nachhinein gehabt, um entsprechende Stellen zu fixen bzw. refactoren.
KLICK
Der zweite Punkt der besser hätte laufen können, war das automatisierte Testen unserer Applikation. Wir hatten darauf plädiert, keine Tests im Frontend zu schreiben, was zum Beispiel dazu geführt hat, dass mehrere Monate lang unser LogOut Knopf nicht funktioniert hat. Und das hat auch keiner bemerkt. Mit automatisierten Tests wären wir unmittelbar darauf aufmerksam gemacht worden, als der Knopf nicht mehr funktioniert hat. User Interfaces zu testen ist immer eine schwierige Diskussion, die die meisten Entwickler gar nicht führen wollen, aber wie man sieht, hat das schon seinen Sinn. In Zukunft würden wir zumindest für grundlegende Funktionalitäten auch im Frontend Tests schreiben. Dazu gibt es auch angemessene Frameworks wie Cypress, mit denen Teile unseres Teams sogar schon Erfahrung haben.
Im Backend hatten wir zwar Tests, allerdings noch nicht von Anfang an. Das Aufsetzen der Testsuite komplizierter war als gedacht, da wir eine Keycloak- und Datenbank-Anbindung in den Tests sicherstellen mussten, und am Anfang andere funktionale Features höher als die Tests priorisiert hatten. Als wir dann die Tests aufgesetzt hatten, ist uns aufgefallen, dass es Fehler in Code gab der schon lange deployt war, was zu unerwartetem Debuggingaufwand gegen Ende der Zeit geführt hat. Unsere Learnings hier sind es die Testumgebung so früh wie möglich aufzusetzen und die Tests dann parallel mit den Features zu schreiben, oder sogar TDD zu verwenden - sprich Tests für Funktionen zu schreiben, bevor wir sie überhaupt programmieren. Dementsprechend wüssten wir dann sofort wann die Funktionalität richtig implementiert ist, da dann auf einmal die Tests durchlaufen.
KLICK
Der letzte Punkt den wir hier aufführen möchten, ist ein eher kleiner Kritikpunkt, bei dem wir uns auch noch nicht sicher sind ob andere Alternativen so viel besser gewesen wären. Es geht hier um die Art wie wir End-To-End-Type-Safety sichergestellt haben. Auf Wunsch von Leonard haben wir dafür im Backend mit Swagger alle Schnittstellen dokumentiert. Das Frontend hat allerdings keinen Zugriff auf den Code im Backend, weshalb wir aus der Swagger-Spezifikation aus dem Backend einen Api-Client generieren müssen, den das Frontend dann verwendet und in dem ebenfalls alle Schnittstellen spezifiziert sind. Das kleine Problem das im Zuge dieser Methode auftrat war, dass der Client bei jeder Änderung des Kommunikationsprotokolls im Backend neu generiert werden musste. Das hat immer etwas Zeit gekostet und außerdem dafür gesorgt, dass unsere CI-Pipelines langsamer laufen, weil auch dort der Client immmer neu generiert werden muss. Eine Verbesserung die wir hätten nutzen können ist tRPC, ein Projekt das sich genau diesem Problem widmet. Ein anderer Gedanke war es, unseren Package-Manager yarn so zu konfigurieren, dass das Front- und Backend einen gemeinsamen Workspace teilen, in dem solche Informationen ähnlich wie in einem Common-Modul vermerkt sind.
Zusammenfassend konnten wir also sehr viel aus dem Projekt lernen und glauben, dass trotz der genannten Punkte die rein technische Umsetzung überwiegend erfolgreich verlaufen ist.