Nach einer längeren Pause geht unsere Blogserie zum Thema GraphQL nun weiter. Viel Spaß beim lesen!

Das Thema Autorisierung ist unter GraphQL von besonderem Interesse. In klassischen REST APIs haben wir verschiedene Ressourcen, auf denen bestimmte Aktionen definiert sind. Nehmen wir als Beispiel eine Kalender Anwendung. Ein Endpoint unter REST wäre z.B. example.com/events. Mit POST werden neue erstellt, mit GET wird eines abgerufen. Welche Aktionen ich durchführen darf, lässt sich relativ einfach festlegen, alle dazu benötigten Informationen lassen sich aus der Anfrage ableiten. 

Unter GraphQL haben wir diese einfache Unterscheidung zwischen den einzelnen Ressourcen nicht. Vielmehr haben wir hier nur einen einzigen Endpunkt, der wiederum verschiedene Queries und Mutations zulässt. Und in dem Ergebnis einer Query können einzelne Knoten aus der Query enthalten sein, oder mit einem Fehler geblockt werden. Welche dieser Knoten angefragt und welche geblockt werden, kann wieder von Anfrage zu Anfrage verschieden sein. In diesem Blogeintrag werden wir Queries und Mutations gleichbehandeln, denn auch wenn jetzt vielleicht der ein oder andere widersprechen will: der einzige echte Unterschied zwischen einer Query und einer Mutation ist der Name und die angedachte Semantik, aber die kann ich als Entwickler biegen und brechen, wie es mir beliebt. Zurück zum Thema, ein kleines Beispiel. 

Wir haben wieder unsere Chat-App mit ihren zwei Microservices und dem kleinen Gateway. Hier wäre es nicht sehr vorteilhaft, wenn wir als Durchschnittsuser die Nachrichten von allen anderen Usern sehen könnten. Doch wie können wir das verhindern? 

Drei Möglichkeiten für die Autorisierung 

Eine Möglichkeit sind Direktiven. Diese können einfach an einen Typen oder ein Feld geschrieben werden. Bevor dieses Feld ausgewertet wird, werden die Direktiven ausgewertet. Wenn eine solche Direktive zu False auswertet oder bei der Auswertung ein Fehler geworfen wird, wird das Feld nicht dargestellt. Damit lassen sich im Schema die einzelnen Rechte darstellen. Das Problem hierbei ist, dass das Schema dadurch mit Informationen überladen wird, die nicht das Datenmodell definieren, sondern andere Aufgaben erfüllen. Auch muss das Schema auf dem System existieren, das den Server definiert. Bei Remote Schemas besteht oft keine Möglichkeit, auf die Direktiven zuzugreifen, zumindest nicht, wenn Introspect Queries genutzt werden, um das Schema auszulesen. Andererseits hat dieses Vorgehen den Vorteil, dass es genau eine Quelle der Wahrheit für eine Schnittstelle gibt und die Informationen nicht über das gesamte System verstreut liegen. 

Die Prüfung kann auch in den Resolvern der Felder selbst passieren. Wenn beim Aufruf von Resolve ein Fehler auftritt (z.B. „AuthenticationError: Unauthorized“), wird das Feld auch nicht dargestellt. Das kann jedoch schnell zu einer gewissen Vermischung von Anwendungslogik und Rechteverwaltung führen. Natürlich kann die Architektur dementsprechend gestrickt werden, dass die Vermischung nicht zustande kommt, aber es ist recht wahrscheinlich, dass es dennoch passiert. 

Es gibt auch verschiedene Frameworks, mit denen man diese Aufgaben bewältigen kann. So zum Beispiel graphql-shield oder GraphQL Resolvers. Die einzelnen Endpunkte können so recht einfach mit Autorisierungsmethoden umschlossen und geschützt werden.  

Dieser Ansatz trennt die Autorisierung und die Anwendungslogik klar voneinander. In vielen Fällen, in denen GraphQL nur als dünne Schicht über einer bestehenden Anwendung zum Austausch von Daten genutzt wird, ist aber eine dedizierte Autorisierung nicht nötig, da diese im Hintergrund in der bestehenden Businesslogik passiert. 

Über einfache und komplexere Autorisierungsfälle 

Bei der Autorisierung gibt es verschiedene Anwendungsfälle. Einer der einfacheren Fälle ist die Frage, ob ein User eine bestimmte Rolle hat. So dürfen in unserer Chat-App z.B. nur Administratoren alle User einsehen. Das lässt sich recht einfach umsetzten, über die vorangegangene Autorisierung ist die Identität des Users bereits bekannt, eine Zuordnung zu einer Rolle ist recht einfach. Das ist aber nur ein „Alles oder Nichts“ Szenario. Wir könnten uns eine Regel überlegen, die lautet „nur ich darf meine Unterhaltungen sehen“. In diesem Fall müssen wir prüfen, ob der Nutzer, den ich gerade Anfrage, auch tatsächlich ich bin. Auch Anfragen an einen dedizierten Server für die Autorisierungslogik sind denkbar. 

Da wir in unserer Chat-App die Autorisierung an einem zentralen Gateway umsetzen wollen und dort Remote Schemas benutzen, müssen wir die Informationen, die wir für die Autorisierung benötigen, ggf. explizit selbst vom Server holen, da wir der User nicht zwingen wollen, diese Informationen anzugeben. Dazu haben wir das Schema des Gatways, das wir über die Introspect Queries von unseren Microservices bekommen und zusammengesetzt haben, erweitert. So werden immer alle Informationen für die Autorisierung, wie die ID eines Users, mit abgefragt. Der Apollo Server filtert diese Daten dann selbst wieder raus und gibt nur die vom Nutzer angefragten Daten zurück. Das ist ein Ansatz, der auch häufig beim Schema Stitching verwendet wird, da dort oft Informationen benötigt werden, die nicht vom User angefordert wurden. 

Umsetzung in der Chat-App 

Für unsere Chat-App haben wir einen kleinen Wrapper mit Hilfe der GraphQL-tools und dem Package GraphQL Resolvers geschrieben, der das Schema für uns erweitert. Damit können wir einfach ein Feld angeben, das wir für die Autorisierung benötigen und eine Methode, die mit diesem Feld (beispielsweise der UserId eines Users), die Rechte prüft. Die Konfiguration erfolgt dann über ein Objekt, das z.B. so aussehen kann: 

const authorization = {
Query: {
                users: { resolver: isAdmin },
                me: { resolver: anyUser }
        },
        User: {
                conversations: {
                        dependsOn: ‚id‘,
                        resolver: isMe,
                }
        }
 

Damit können wir zu jedem Feld angeben, welche Abhängigkeiten es vom Parent-Objekt hat und welche Methode die Rechte prüfen soll. Wir können auch eine Standardfunktion angeben, die geprüft wird, wenn für ein Feld keine Funktion angegeben ist. Damit kann der Zugriff z.B. auf Administratoren beschränkt werden, außer der Zugriff auf ein Feld wird explizit erlaubt (Whitelisting). Mit dieser Lösung haben wir auch sauber die Autorisierung von der Logik getrennt, die Autorisierung kann so auch an unser Gateway ausgelagert werden. 

Schlussworte 

Autorisierung ist möglich und die Möglichkeiten dazu sind sehr vielfältig. Grundsätzlich empfiehlt es sich, den Zugriff auf ganze Knoten oder gar Typen nach bestimmten Kriterien einzuschränken. Wenn nur lokale Schemata benutzt werden, sind häufig mehr Informationen vorhanden, als der User anfragt, oder zusätzliche Informationen können einfach aus der Datenbank gelesen werden. Wenn jedoch Remote Schemas genutzt werden, ist die Sache etwas kniffliger, aber auch sauber zu lösen. 

Alle Beiträge von Manuel Styrsky

Schreibe einen Kommentar