In diesem Kapitel schauen wir uns die Sofa Validierungsfunktionen genauer an. Sofa benutzt grundlegende Validierungsfunktionen, die man in einer Anwendung benötigt. Wenn man diese Funktionen versteht, dann hat man genug Grundwissen um eigene Funktionen in weitere Anwendungen zu schreiben.
CouchDB benutzt die validate_doc_update
Funktion um unautorisierten Zugriff sowie ungültige oder unerwünschte Datenupdates zu verhindern. In der Beispielanwendung benutzen wir die Funktion, um sicherzustellen, dass Blogeinträge nur von autorisierten Benutzern bearbeitet werden können. Bei den CouchDB Validierungsfunktionen - genau wie bei den map und reduce Funktionen - werden keine Nebenläufigen Operationen ausgeführt, da diese isoliert von der Anfrage abgearbeitet werden. Man kann nicht nur direkte Zugriffe, sondern auch entfernte Zugriffe von anderen CouchDBs verhindern.
Um sicherzustellen, das Benutzer nur Dokumente speichern, dessen Felder auch gültig sind, benötigt man nur dem _design
Dokument, die validate_doc_update
Funktion hinzuzufügen. Das ist das erste mal, dass die CouchDB einen externen Prozess ausführt. Die CouchDB sendet Funktionen und Dokumente zu einem externen JavaScript Interpreter, was uns erlaubt, das wir die Validierungsfunktionen in CouchDB realisieren können. Die validate_doc_update
Funktion wird für jedes Dokument das neu angelegt oder gespeichert wird, ausgeführt. Wenn die Validierung fehlschlägt und einen Fehler wirft, wird der Updatevorgang abgebrochen; wenn nicht, wird das Update ausgeführt.
Die Validierung des Dokumentes ist optional. Wenn keine Validierungsfunktionen existieren, dann wird keine Eingabe überprüft und alle Daten können in die Datenbank geschrieben werden. Wenn mehrere Design Dokumente mit einer validate_doc_update
Funktion existieren, dann wird bei einer ankommenden Anfrage jede Validierungsfunktion von jedem Designdokument aufgerufen und auf die Anfrage angewandt. Nur, wenn kein Fehler in dem gesamten Validierungsprozess auftreten, werden die Daten in die Datenbank geschrieben. Die Reihenfolge, in welcher die Validierungsfunktionen aufgerufen werden ist nicht festgelegt, so, dass jede Funktionen für sich selbst arbeiten muss und keine Abhängigkeiten zu den anderen Funktionen nutzen kann. Siehe auch Figur 1, “Die JavaScript, Dokumentvalidierungsfunktion”.
Validierungsfunktionen können Updates abbrechen, indem sie Fehler werfen. Um einen Fehler zu werfen, welcher zeigt, das der Anwender nicht autorisiert ist, die Abfrage durchzuführen, sieht der JavaScript-Code so aus:
throw({unauthorized : message});
Um zu verhindern, das ein autorisierter Anwender, ungültige Daten in einen Datensatz schreibt, kann dieser Fehler geworfen werden:
throw({forbidden : message});
Diese Funktion wirft einen forbidden
Fehler, wenn die ankommenden Daten nicht gültig sind. Zusätzlich werden auch validate()
Helfer eingesetzt um die Daten zu validieren. Es werden auch einfache JavaScript Konditionen genutzt um sicherzustellen, dass doc._id
den selben Inhalt wie doc._slug
besitzt. Dies ist nützlich um schöne URLs zu formen.
Wenn keine Fehler geworfen werden, dann sieht CouchDB die einkommenden Daten als gültig an und schreibt diese in die Datenbank. Indem wir JavaScript verwenden um JSON Dokumente zu validieren, können wir jede mögliche erlaubte JSON-Struktur nutzen. Dadurch kann jedes gültige Dokument einfach und schnell validiert werden. Validierung ist auch eine gute Form der Dokumentation.
Bevor wir weiter ins Detail gehen, beschäftigen wir uns zuerst mit der Funktion der Validierung.
Validierungsfunktionen werden in design documents
in dem Feld validate_doc_update
gespeichert. Es gibt nur eine Validierungsfunktion pro Designdokument, aber es gibt mehrere Designdokumente in einer Datenbank. Beim speichern eines Datensatzes wird dieser, durch alle Validierungsfunktionen, aller Designdokumenten durchlaufen. (Die Reihenfolge der Abarbeitung ist nicht definiert.) In diesem Kapitel gehen wir einfachhalber davon aus, das es nur eine einzige Validierungsfunktion gibt.
Die Funktionsdeklaration ist denkbar einfach. Es werden 3 Argumente erwartet: das veränderte Dokument, die aktuelle Version des Dokuments, und das entsprechende Objekt des Benutzers, welcher die Anfrage ausgeführt hat.
function(newDoc, oldDoc, userCtx) {}
Dies ist die einfachste Validierungsfunktion, welche beim verändern der Daten, egal um welche Daten oder welchen Benutzer es sich handelt, alles zulässt. Umgekehrterweise sieht die Funktion die alles blockiert, so aus:
function(newDoc, oldDoc, userCtx) { throw({forbidden : 'no way'}); }
Beachte, dass durch diese Funktion in der Datenbank, solange keine Daten mehr verändert werden können, bis diese aus dem Designdokument entfernt wurde. Administratoren können trotz dieser Einschränkungen Designdokumente bearbeiten, so, dass diese Funktion auch wieder entfernt werden kann.
Anhand dieser Beispiele können wir sehen, das die return-Anweisung ignoriert wird. Validierungsfunktionen verhindern Updates mithilfe von Exceptions. Wenn ein Update ohne Fehler alle Validierungsfunktionen durchläuft, dann wird dieses ausgeführt und fest in die Datenbank geschrieben.
Das wichtigste einer Validierungsfunktion ist, sicherzustellen, dass Dokumente alle Felder enthalten die eine Anwendung benötigt. Ohne Validierung, müssten man die Vollständigkeit eines Dokumentes in den MapReduce Funktionen sicherstellen. Mit Validierung weiß man, dass alle benötigten Felder beim speichern vorhanden sind.
Ein übliches Muss in vielen Sprachen, Frameworks und Datenbanken ist, das Benutzen von unterschiedlichen Typen um zwischen Datensätzen zu unterscheiden. Als Beispiel haben wir in Sofa die Dokumenttypen post
und comment
.
CouchDB selber ist typenlos, aber sie sind ein praktisches Hilfsmiitel in Anwenungen, den MapReduce Views und dem Interface. Die Konvention besagt, dass es ein Feld namens type
innerhalb der einzelnen Dokumente geben sollte, wobei viele Frameworks andere Felder benutzen. Wie der Benutzer das Feld nennt ist ihm überlassen. (Zum Beispiel nennt der CouchRest Ruby Client das Feld couchrest-type
)
Hier ist eine Beispiel Validierungsfunktion welche nur auf Posts angewandt wird:
function(newDoc, oldDoc, userCtx) { if (newDoc.type == "post") { // validation logic goes here } }
Da CouchDB in jedem Designdokument nur eine Validierungsfunktion zulässt, kann man auch mehrere Typen innerhalb einer Validierung abarbeiten. Die Funktion könnte dann so aussehen:
function(newDoc, oldDoc, userCtx) { if (newDoc.type == "post") { // validation logic for posts } if (newDoc.type == "comment") { // validation logic for comments } if (newDoc.type == "unicorn") { // validation logic for unicorns } }
Es sei nochmals gesagt, dass type
ein optionales Feld ist. Wir stellen es hier als eine nützliche Technik vor, um den Validierungsprozess zu vereinfachen. Es gibt auch andere Wege eine solche Funktion zu schreiben. Hier ist ein Beispiel das duck typing, anstatt von einem type
Attribut benutzt:
function(newDoc, oldDoc, userCtx) { if (newDoc.title && newDoc.body) { // validate that the document has an author } }
Dieser Weg der Validierung ignoriert das type
Attribut völlig. Stattdessen wird hier davon ausgegangen, dass Dokumente, welche einen Titel und einen Body haben, auch einen Autor haben müssen. Für machen Anwendungen ist die typenlose Validation einfacher. Wiederrum kann es auch ein Nachteil sein, wenn andere Felder ihre Abhängigkeiten kennen.
In der Praxis nutzen viele Anwendungen eine Mischung aus typenlosen und typisierten Validierungen. Sofa benutzt zum Beispiel Dokumenttypen, um nachzuvollziehen welche Felder eines Dokuments benötigt werden. Es wird aber auch Duck Typing genutzt, um die Struktur von speziellen Feldern zu validieren. Es ist egal welche Sorte von Dokumenten wir validieren. Wenn das Dokument eine created_at
Feld hat, dann können wir sichergehen, dass das Feld einen passenden Timestamp beinhaltet. Das ist vergleichbar damit, dass wenn wir einen Autor von einem Dokument validieren, es uns egal ist, welchen Typ dieser hat. Wir wissen nur, dass der Autor dem Benutzer gleicht, welcher das Feld gespeichert hat.
Die wichtigste Form der Validierung ist, das wir sicherstellen, dass spezielle Felder in einem Dokument vorhanden sind.
Pflichtfelder machen die Anzeigenlogik viel einfacher. Nichts ist dilettantischer als wenn das Wort undefined
zurückgegeben wird. Wenn ein Feld mit Sicherheit in einem Dokument immer vorkommt, dann kann man sich umständliche Abfrage aber auch sparen.
Sofa benötigt verschiedene Felder bei Posts und Comments. Hier ist ein Ausschnitt einer Validierungsfunktion:
function(newDoc, oldDoc, userCtx) { function require(field, message) { message = message || "Document must have a " + field; if (!newDoc[field]) throw({forbidden : message}); }; if (newDoc.type == "post") { require("title"); require("created_at"); require("body"); require("author"); } if (newDoc.type == "comment") { require("name"); require("created_at"); require("comment", "You may not leave an empty comment"); } }
Das ist unser erster Blick auf die Validierungslogik. Man kann sehen, dass der Codeabschnitt, welcher den Fehler wirft, in eine extra Funktion ausgelagert wurde. Helfer wie die require
Funktion machen den Code einfach und leserlich. Die require
Funktion ist dabei sehr einfach gehalten, sie erwartet ein Feld Name und eine optionale Message und stellt sicher, dass das Feld nicht leer ist.
Wenn wir einmal die Helferfunktion deklariert haben, können wir diese immer wieder verwenden. Posts benötigt einen title
, einen timestamp
, einen body
, und einen author
. Comments benötigt einen name
, einen timestamp
und den comment
selber. Wenn wir sicherstellen wollen, das jedes Dokument ein Feld created_at
enthalten muss, dann können wir dies ausserhalb der Typendeklaration setzen.
Timestamps sind ein interessantes Problem bei der Validierungsfunktion, da die Validierung nicht on-the-fly, sondern mit einer, wenn auch geringen, Zeitverzögerung ausgeführt wird. Wir können nicht erwarten, das der Timestamp mit dem Servertimestamp übereinstimmt. Wir können von 2 Dingen ausgehen: Timestamps verändern sich nicht, nachdem diese gesetzt wurden und sie sind haben ein richtiges Format. Was es heisst, das Timestamps im richtig Format gesetzt werden schauen wir uns anhand von Sofa an.
Als erstes gucken wir auf den Validationshelfer, welcher es nicht erlaubt, verhandene Felder zu verändern,
function(newDoc, oldDoc, userCtx) { function unchanged(field) { if (oldDoc && toJSON(oldDoc[field]) != toJSON(newDoc[field])) throw({forbidden : "Field can't be changed: " + field}); } unchanged("created_at"); }
Der unchanged
Helfer ist ein bisschen komplexer als der require
Helfer. Die erste Zeile in der Funktion verhindert das ausführen der Funktion bei Updates. Der unchanged
Helfer kümmert sich nicht um den Inhalt des Feldes, welcher das erste mal gesetzt wird. Wenn also eine gespeicherte Version des Feldes schon existent ist, fordert der unchanged
Helfer, das die neue, sowie die alte Version, gleich sind.
JavaScript’s Gleichheitsprüfung ist nicht geeignet um mit tieferen verschachtelten Objekten zu arbeiten. Wir nutzen die von der CouchDB integrierten toJSON
JavaScript Funktion in unseren vergleichen, da diese besser geeignet ist, um die Objekte miteinander vergleichen zu können. Hier sieht man warum:
js> [] == [] false
JavaScript sieht diese Arrays nicht als gleich an, da nicht der Inhalt bei der Überprüfung als Grundlage genommen wird, sondern diese als Objekte gesehen werden. Wir benutzen die toJSON
Funktion um das Objekt zu einem String zu konvertieren, da die Stringvergleiche den Inhalt betrachten und wir dadurch die Gleichheit zweier Objekte mit demselben Inhalt feststellen können.
Der js
Befehl wird mitinstalliert, wenn auch CouchDB´s SpiderMonkey Abhängigkeit mitinstalliert wird. Dies ist eine Kommandozeilen Anwendung welche es erlaubt JavaScript Code on-the-fly zu parsen, evaluieren und auszuführen. Mit js
kann man, wie oben gesehen, schnell JavaScript code testen. Man kann auch einen Syntaxcheck einer Datei mittels js file.js
durchführen. Meistens sind die CouchDB Fehlerausgaben nicht sehr hilfreich, daher ist es öfter besser den Code eigenständig zu testen und eine aussagekräftige Fehlermeldung zu erhalten.
Die Rechteverwaltung ist innerhalb von verteilten Systemen eine interessante Frage. In manchen Umgebungen kann man dem Server vertrauen, wenn dieser die Autorschaft zu einem Dokument setzt. Zurzeit hat die CouchDB ein einfaches eingebautes System um die Rechte der node admins zu verwalten. Es gibt die Möglichkeit, innerhalb einer Datenbank, Administratorrechte zu vergeben, genau wie man andere Rechte vergeben kann. Die Authentifizierung der Benutzer zur CouchDB kann man über eine HTTP Schicht, welche LDAP nutzt, oder andere Wege realisieren.
Sofa nutzt den intigrierten vernetzten Administratorenaccount, da dieser am besten für einzelne oder größere Gruppen geeignet ist. Sofa so zu erweitern, das zusätzliche Autoreninformationen in der Datenbank gespeichert werden, soll die Aufgabe des Lersers sein.
Sofa’s Validierungslogik besagt, dass nur ein Autor, welcher in der Autorenliste gelistet ist, Dokumente speichern darf:
function(newDoc, oldDoc, userCtx) { if (newDoc.author) { enforce(newDoc.author == userCtx.name, "You may only update documents with author " + userCtx.name); } }
Validierungsfunktionen sind ein starkes Mittel um sicherzustellen, dass nur die Dokumente die man erwartet auch in die Datenbank geschrieben werden. Es gibt 3 mögliche Formen auf welche wir bei der Validierung zurückgreifen können. Der Inhalt, die Struktur und derjenige, welcher die Anfrage ausgeführt hat. Zusammen sind diese 3 Validierungsmöglichkeiten ausreichend, um komplexe Routinen zu schreiben, die jeden davon abhalten die Daten der Datenbank zu manipulieren oder manipulierte Daten in die Datenbank zu schreiben.
Mit Sicherheit sind die Validierungsfunktionen kein vollständiger Ersatz für Sicherheitssysteme, obwohl diese mit anderen Sicherheitsmechanismen sehr gut zusammenarbeiten. Mehr über CouchDB´s Sicherheit in Kapitel 22, Sicherheit.