Suche
Knoten und Kanten

Daten in SQL-Datenbank und ACL in Neo4j – funktioniert das?

Daniel Brenner, Stefan Zilch

© Shutterstock / vetre

Besonders im Unternehmensumfeld ist in vielen Fällen eine einfache Authentifizierung beim Zugriff auf sensible Daten nicht ausreichend, weshalb zusätzlich eine Autorisierung erforderlich wird. Diese stellt u. a. sicher, dass der Benutzer innerhalb des Systems nur dann Daten sieht oder bearbeiten kann, wenn er die notwendigen Rechte besitzt. Die Abbildung von komplexen Rechtestrukturen, Benutzergruppen, Rollen und vererbbaren Rechten ist dabei keine triviale Aufgabe. Lesen Sie, wie ein passender Lösungsansatz mit der Graphendatenbank Neo4j aussieht.

Denken Entwickler über ein Rechtesystem nach, fällt ihnen vermutlich erst einmal der Klassiker ein: die typischen CRUD-Rechte (Create, Read, Update und Delete), die in Anwendungen immer wieder modelliert und umgesetzt werden müssen. Wie auch im folgenden Bespiel: In einer Abteilung eines Versicherungsunternehmens wird die Bearbeitung von Schadensfällen aufgeteilt: Team 1 bearbeitet die Schadensfälle von A bis L und Team 2 die Fälle von M bis Z. Beide Teams können neue Schadensfälle anlegen sowie bestehende in ihrem Bereich bearbeiten und löschen. Damit die Sacharbeiter alle Schadensfälle vergleichen können, kann auf jeden Schadensfall lesend zugegriffen werden. Basierend darauf lassen sich folgende Regeln ableiten:

  • Es gibt drei Benutzergruppen „Abteilung“, „Team 1“ und „Team 2“.
  • Die Benutzergruppen „Team 1“ und „Team 2“ gehören zur Benutzergruppe „Abteilung“.
  • Alle Mitglieder der Benutzergruppe „Team 1“ dürfen Schadensfälle von A bis L aktualisieren (Update).
  • Alle Mitglieder der Benutzergruppe „Team 1“ dürfen Schadensfälle von A bis L löschen (Delete).
  • Alle Mitglieder der Benutzergruppe „Team 2“ dürfen Schadensfälle von M bis Z aktualisieren (Update).
  • Alle Mitglieder der Benutzergruppe „Team 2“ dürfen Schadensfälle von M bis Z löschen (Delete).
  • Alle Mitglieder der Benutzergruppe „Abteilung“ dürfen alle Schadensfälle lesen (Read).
  • Alle Mitglieder der Benutzergruppe „Abteilung“ dürfen neue Schadensfälle anlegen (Create).

Die Liste der Regeln demonstriert, dass solch einfaches Beispiel relativ schnell sehr komplex werden kann. Daher zeigt Abbildung 1 den Zusammenhang in grafischer Form.

Abb. 1: Visualisierte Rechtestruktur des Beispiels

Abb. 1: Visualisierte Rechtestruktur des Beispiels

Knoten sind die Benutzer („B1…B7“ bzw. „B2…B8“) und Benutzergruppen („Team 1“, „Team 2“ und „Abteilung“) sowie die Schadensfälle von A bis Z. Die (gerichteten) Kanten zwischen den einzelnen Knoten bilden unterschiedliche Beziehungen ab, wobei die Beschriftung die Relationsart der beiden Knoten darstellt. So markiert zum Beispiel die Kante zwischen zwei Benutzergruppen die Beziehung „Mitglied“. Benutzergruppe „Team 1“ (analog „Team 2“) ist Mitglied in der Benutzergruppe „Abteilung“. Vom Knoten „Team 1“ geht eine weitere Beziehung zum Knoten „Schadensfall A“, der die Rechte „Update“ und „Delete“ abbildet. Damit ist festgelegt, welche Rechte die Benutzergruppe , d. h. deren Mitglieder, auf Schadensfall A hat.

Beim Knoten „Schadensfall“ handelt es sich um den Datentyp, der als eigener Knoten abgebildet wird. Dieser wird eingeführt, um auf einer Metaebene Rechte vergeben zu können und somit das Rechtesystem zu vereinfachen. Die Beziehung zwischen den Knoten „Abteilung“ und „Schadensfall“ bildet dabei die Rechte „Create“ und „Read“ ab. Das bedeutet, dass alle Mitglieder der Benutzergruppe „Abteilung“ neue Objekte vom Typ „Schadensfall“ anlegen (Create) und alle Objekte vom Typ „Schadensfall“ lesen (Read) dürfen. Eine Vergabe des Rechtes „Create“ auf Objektebene ist wenig sinnvoll, da dieses Objekt bereits angelegt wurde und nicht noch einmal angelegt werden kann. Im Fall des Rechtes „Read“ ist es eine Vereinfachung, damit alle Mitglieder der Benutzergruppe „Abteilung“ alle Objekte vom Typ „Schadensfall“ lesen dürfen. Ist eine solche generelle Vergabe von Rechten nicht gewünscht, dann kann es zum Beispiel eine Beziehung von der Benutzergruppe „Team 1“ zu „Schadensfall“ geben, die das Recht „Read“ abbildet.

Es wäre sogar eine noch feinere Eingrenzung möglich, indem man das Leserecht zwischen einzelnen Benutzern und Schadensfällen über Kanten abbildet. In der Praxis sollte die Anforderung für solche feingranularen Fälle unbedingt gegeben sein, da dadurch Komplexität und Wartbarkeit bedeutend ansteigen.

Direkte und indirekte Benutzergruppen auffinden

Wie funktioniert nun das Rechtesystem, wenn ein Benutzer einen Schadensfall aktualisieren möchte? Dazu werden natürlich der Benutzername und die ID des Schadenfalls benötigt, die folgendermaßen ermittelt werden: Ein Benutzer kann in mehreren Benutzergruppen Mitglied sein. Weiterhin ist die Verschachtelung von Benutzergruppen möglich, was dazu führt, dass ein Benutzer auch indirekt zu einer Benutzergruppe gehören kann. Das ist wichtig, da Rechte auch aus indirekten Gruppen geerbt werden können. Zum Beispiel ist „Benutzer 1“ Mitglied in „Team 1“, und „Team 1“ gehört wiederum zur „Abteilung“. Somit ist der Benutzer auch Mitglied der Abteilung.

Benutzt eine Anwendung eine relationale SQL-Datenbank, werden in der Regel sowohl die Schadensfälle als auch das Rechtesystem in ihr abgelegt. Dazu gehören u. a. die Benutzer, die Benutzergruppen, die möglichen Rechte (hier CRUD) und deren Beziehungen untereinander. Zur Abbildung von Daten und Rechtesystemen sind mehrere Tabellen nötig, was wiederum bedeutet, dass bei der Abfrage von Datensätzen JOINs zum Einsatz kommen. Bei komplexen Rechtesystemen und großen Datenmengen kann es schnell zu weiteren Herausforderungen kommen: unübersichtliche und sogar performancekritische SQL-Statements.

Rechte prüfen

Für alle direkten und indirekten Benutzergruppen wird dann geprüft, ob eine „Update“-Beziehung zum Schadensfall mit der entsprechenden ID existiert. Ist das der Fall, darf der Benutzer den Schadensfall aktualisieren. Der aktualisierte Schadensfall wird dann in der Datenbank gespeichert.

Aus dem vorgestellten Vorgehen wird ersichtlich, dass es sich bei SQL-Statements zur Bestimmung, ob der Benutzer den Schadensfall aktualisieren darf, um keine trivialen Statements handelt. Schon beim rekursiven Auffinden aller Benutzergruppen wird deutlich, dass auch hier gegebenenfalls teure SQL-JOINs zum Einsatz kommen können. Das Prüfen, bevor eine Operation ausgeführt werden darf, gilt für jedes Recht, das im System vorhanden ist.

Neo4j als alternativer Lösungsansatz

Abbildung 1 visualisiert den Aufbau des Rechtesystems mit allen benötigten Objekten und Beziehungen. Die Idee hinter einer Graphendatenbank ist das Arbeiten mit Knoten und Kanten, das sich sehr gut für die technische Umsetzung unserer Autorisierungsanforderungen eignet.

Neo4j ist eine solche Datenbank, die durch Knoten- und Kantenelemente einen Graphen aufbaut, der Beziehungen zwischen einzelnen Objekten darstellt und somit Datenmodelle abbilden kann. Das Traversieren bzw. Navigieren durch den Graph dient der Beantwortung von Anfragen und ist Kernfunktionalität von Neo4j. Solche Anfragen können dabei auch auf „Schlüssel=Wert“-Paare gestellt werden, die an Knoten und Kanten gespeichert werden, ohne dabei nach einem Schema, wie bei relationalen Datenbanken, zu verlangen. Als Abfragesprache kommt Cypher zum Einsatz.

Die Syntax von Cypher ist speziell an die Struktur von Graphen angepasst. Listing 1 zeigt das Cypher-Statement, mit dem geprüft werden kann, ob „Benutzer 8“ das Recht hat, einen speziellen Schadensfall zu aktualisieren. Nur wenn das der Fall ist, wird die Aktualisierung auch gespeichert.

MATCH (b:Benutzer)-[:Mitglied*]->(Benutzergruppe)-[r:Recht]->(schadensfall:Schadensfall)
WHERE b.name="Benutzer 8" AND r.permission="U" AND schadensfall.zeichen="M"
RETURN schadensfall

Das Statement startet bei einem Benutzer (Knoten). Dann wird über die Kante „Mitglied“ zu einer Benutzergruppe navigiert. Anschließend wird geprüft, ob ausgehend von der Benutzergruppe eine Kante mit dem Recht „Update“ existiert, die zu dem gesuchten Schadensfall führt. Dabei werden die Bedingungen der WHERE-Klausel berücksichtigt. Im Erfolgsfall wird der gefundene Schadensfall zurückgegeben.

Im Statement selbst werden Knoten mit runden und Beziehungen mit eckigen Klammern angegeben. Die Klammern können leer bleiben und haben dann die Bedeutung von „irgendein“ Knoten bzw. „irgendeine“ Beziehung. Es kann aber auch der Typ angegeben werden (nach dem Doppelpunkt, siehe Listing 1). Vor dem Doppelpunkt kann ein Name stehen, der es ermöglicht, diesen Knoten (Beziehung) zum Beispiel in WHERE oder RETURN zu referenzieren. Die Richtung einer Kante kann auch spezifiziert werden (als <–, — oder –>). Für den Einstieg bzw. Umstieg von SQL auf Cypher bietet Neo4j einen empfehlenswerten Guide an, der auch Beispiele enthält.

Best Practices im Lösungsansatz

Der Lösungsansatz, den wir hier vorstellen wollen, basiert auf zwei Datenbanken. Primär werden Nutzdaten in einer SQL-Datenbank abgelegt, dazu gehören neben den Schadensfällen auch die Benutzer und Benutzergruppen. Für die Abbildung der Rechte wird Neo4j verwendet. Darin werden die IDs der Objekte (Datensätze) sowie die Mitgliedschaften in Benutzergruppen und die Rechte auf Schadensfälle durch einen Graphen realisiert. Der Ablauf bei einer create-Operation stellt beispielsweise wie folgt dar:

  1. Prüfe, ob der Benutzer die gewünschte Operation ausführen darf.
  2. Wenn ja: lege neuen Schadensfall in SQL-Datenbank an.
  3. Speichere Rechte für den neuen Schadensfall in Neo4j.
  4. Bei einem Fehler innerhalb der vorherigen Schritte: Rollback beider Datenbanken.
Abb. 2: Ablauf beim Anlegen eines neuen Datensatzes

Abb. 2: Ablauf beim Anlegen eines neuen Datensatzes

Transaktionssicherheit

Wichtig ist, dass alle Schritte in einer Transaktion ausgeführt werden. So kann im Falle eines Fehlers die Transaktion zurückgerollt werden und keine inkonsistenten Daten entstehen. Neo4j bietet, wie gängige SQL-Datenbanken, eine volle ACID-Unterstützung, wodurch es möglich ist, eine Transaktionsklammer um beide Datenbanken zu legen und somit die geforderte Transaktionssicherheit herzustellen. Wie diese Transaktionsklammer jedoch realisiert wird, hängt von den einbezogenen Datenquellen ab und ist somit spezifisch für jede Anwendung.

Verringerung der technischen Komplexität

Wie ist das nun, wenn ein Benutzer Mitglied von direkten und indirekten Benutzergruppen ist und alle Schadensfälle des Buchstaben M sucht, auf die er Schreibrechte hat? Rekursives Auffinden von Benutzergruppen, deren Rechte und die zugehörigen Schadensfälle kann in SQL zu komplexen JOINs oder mehreren SQL Statements führen, deren Daten dann zum Beispiel im Java-Code nochmals nachbearbeitet werden müssen. Solche Stellen können schnell sehr komplex werden und sind nur schwer zu warten. Durch die Verwendung von Neo4j und Cypher werden Abfragen einfacher, da zum Beispiel komplexe JOINs und das Lösen rekursiver Fragestellungen (direkte und indirekte Benutzergruppen) durch das Traversieren im Graphen gelöst werden können.

Speichert man den gesamten Datensatz eines Schadenfalls in Neo4j, könnten Filterkriterien in Form von Attributen (Schlüssel-Werte-Paar) am Datensatz definiert und in der Abfrage berücksichtigt werden. Der Flexibilität und Kreativität sind hier keine Grenzen gesetzt, da man nicht wie bei SQL einem Schema zu folgen hat.

Die technische Umsetzung mit Neo4j

Ein eigenes Rechtesystem zu entwickeln, ist nicht nur aufwändig, sondern birgt häufig auch Risiken. Deshalb ist es sinnvoll, auf bereits vorhandene und vor allem produktiv erprobte Frameworks wie Apache Shiro oder Spring Security zurückzugreifen. Obwohl Spring-Data Neo4j unterstützt, gibt es eine solche offizielle Unterstützung für Spring-Security leider noch nicht. Erfreulicherweise gibt es jedoch eine Open-Source-Implementierung, die Spring-Security mit Neo4j umsetzt.

Ein zentraler Service ist org.springframework.security.acls.model.MutableAclService. Listing 2 zeigt, wie ausgelesen werden kann und welche Rechte ein Benutzer in Form einer sid auf einen bestimmten Schadensfall (oid) hat. Damit kann zum Beispiel die Frage beantwortet werden, ob der Benutzer die Rechte hat, den Schadensfall zu löschen.

ObjectIdentity oid = new ObjectIdentityImpl(
Schadensfall.class, schadensfallM.getId());
Sid sid = new GrantedAuthoritySid(benutzer2.getId());
Map<ObjectIdentity, Acl> acls = mutableAclService.readAclsById(
Collections.singletonList(oid), Collections.singletonList(sid));
Acl acl = acls.get(oid);

List<AccessControlEntry> entries = acl.getEntries();
// ist BasePermission.DELETE enthalten in entries?

Listing 3 zeigt das Anlegen von Rechten, ebenfalls mithilfe des MutableAclService. Mit der Operation createAcl() wird eine Access Control List (ACL) erstellt. Diese wird dann mit den entsprechenden Rechten befüllt und die bestehende ACL aktualisiert. Befüllt wird die ACL mit BasePermission-Objekten. Neben den BasePermissions CREATE, READ, WRITE und DELETE gibt es auch noch ADMINISTRATION. Ob und wie dieses spezielle Recht eingesetzt wird, muss für jede Anwendung im Einzelfall entschieden werden.

Authentication auth = new TestingAuthenticationToken("login", "pwd");
auth.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(auth);
ObjectIdentity oid = new ObjectIdentityImpl(
Schadensfall.class, schadensfall.getId());
MutableAcl acl = mutableAclService.createAcl(oid);

Sid sid = new GrantedAuthoritySid(team1.getId());
acl.insertAce(0, BasePermission.READ, sid, true);
acl.insertAce(1, BasePermission.WRITE, sid, true);
acl.insertAce(2, BasePermission.DELETE, sid, true);

mutableAclService.updateAcl(acl);

Soll das Abspeichern eines Schadenfalls und der entsprechenden Rechte in einer Transaktion erfolgen, so kann die @Transaction-Annotation verwendet werden. Bei entsprechender Spring-Konfiguration wird dann je nach Kontext eine neue Transaktion angelegt, bzw. die vorhandene weiter verwendet. Der Beispielcode zum Artikel ist unserem GitHub-Account verfügbar.

Weiterführende Ideen und Ausbaustufen

Das hier beschriebene Rechtesystem ist einfach und doch vollkommen ausreichend für die meisten Anwendungen. Es gibt aber noch darüber hinausgehende Anforderungen, die bis jetzt nicht betrachtet worden sind.

Geheimhaltungsgrade

Zu diesen weiteren Anforderungen gehören Geheimhaltungsstufen („Clearance Level“). Mögliche Stufen der Geheimhaltung sind „öffentlich“, „vertraulich“ und „geheim“. Die Geheimhaltungsstufen sind den Daten, im Beispiel also den Schadensfällen, zugeordnet. Außerdem hat jeder Benutzer eine definierte Einstufung. Bei der Abfrage wird die Einstufung des Benutzers mit der Geheimhaltungsstufe des angefragten Schadensfalls verglichen. Ist die Einstufung des Benutzers gleichwertig oder höher, dann darf er den Schadensfall sehen. Ob die Einstufung des Benutzers nun direkt am Benutzer hängt oder ob dieser die Einstufung durch die Mitgliedschaft in einer Benutzergruppe erhalten hat, kann in diesem Fall vernachlässigt werden, da wichtig ist, dass er überhaupt eine hat.

Rollen

Eine weitere Anforderung sind Rollen, ein bekanntes Beispiel für eine Rolle ist „Administrator“. Bei Rollen handelt es sich um spezielle Sonderrechte, die ein Benutzer aufgrund seiner Funktion hat. So bedeutet die Rolle „Administrator“, dass dieser Benutzer über alle Rechte verfügt. Oft wird „Administrator“ auch als Recht abgebildet zwischen einer Benutzergruppe auf der einen und dem Typ von Objekten auf der anderen Seite. Das ist nach dem RBAC-Ansatz jedoch nicht korrekt, da in diesem Fall die Rolle einer Benutzergruppe entspricht.

Temporäre Erweiterung von Rechten

Eine Urlaubsvertretung ist sinnvollerweise nicht als eine Rolle realisiert, sondern über einen temporären Fremdzugriff. Das ist keine Beziehung zwischen Benutzer/Benutzergruppe und Objekt, sondern zwischen Benutzer/Benutzergruppe und einem anderen Benutzer bzw. einer anderen Benutzergruppe. Die Kante würde in diesem Fall den Namen „Fremdzugriff“ erhalten. Hat Benutzer 6 zum Beispiel eine Kante „Fremdzugriff“ auf Benutzer 7, dann bedeutet dies, dass Benutzer 6 zu seinen eigenen Rechten auch noch die kompletten Rechte von Benutzer 7 erhält. Solche Fremdzugriffsrechte werden normalerweise nur zeitlich begrenzt, zum Beispiel beim Urlaub, vergeben. Je nach Anwendung ist es sinnvoll, den Fremdzugriff in unterschiedlichen Ausprägungen zu realisieren: Beim lesenden Fremdzugriff darf Benutzer 6 die Daten von Benutzer 7 nur lesen, unabhängig davon, welche Rechte Benutzer 7 wirklich auf die Daten hat.

Verfeinerung der Granularität von Zugriffsrechten

Das beschriebene Beispiel zeigt momentan nur die Vergabe von Zugriffsrechten auf eine gesamte Klasse (Schadensfall). Abhängig vom Anwendungsfall kann es jedoch auch notwendig sein, die Rechte weiter aufzugliedern, um somit einen Zugriff auf einzelne Attribute einer Klasse einzuschränken. Das erlaubt eine sehr feingranulare Vergabe von Rechten, je nach konkretem Bedarf. Im oberen Beispiel könnte das der Zugriff auf die beim Schadensfall hinterlegten Kontodaten sein. Die Mitglieder von „Team 1“ und „Team 2“ dürfen die Kontodatenfelder nicht sehen, der Benutzer „Abteilungsleiter“ jedoch schon. Konsequenterweise muss in diesem Fall aber auch die Eingabemaske befähigt werden, nur diejenigen Felder anzuzeigen, die ein Benutzer sehen darf.

Fazit und Ausblick

Die Anforderung einer Autorisierung gehört heute schon fast zum Alltag, wenn eine individuelle Anwendung für Kunden entwickelt wird. Sind die Anforderungen analysiert und ist das Rechtekonzept ausgearbeitet, folgt die technische Realisierung. Hierzu bekommt der Entwickler bereits mit stabilen und ausgereiften Frameworks wie Spring-Security eine erstklassige Unterstützung. Eine populäre Implementierung des ACL-Konzepts basiert auf SQL-Datenbanken, die ihrer Aufgabe für die meisten Fälle auch gerecht werden. Doch manchmal reicht der Standardfall eben nicht aus und die Komplexität der Zugriffssteuerung steigt rasant an. Dabei entstehen u. a. aufwändige und teure Datenbankabfragen, ggf. zusätzlicher Code, der Auswirkungen auf Performance, Wartbarkeit und Weiterentwicklung haben kann.

Mit der Verwendung einer Graphendatenbank, hier Neo4j, haben wir eine mögliche Alternative vorgestellt, die gerade bei komplexen Zusammenhängen punkten kann. Das Rechtesystem kann mit dem Kunden graphisch durch die Verwendung von Knoten und Kanten erarbeitet und dann fast eins zu eins in Neo4j umgesetzt werden. Mithilfe der Abfragesprache Cypher lassen sich dabei aufwändige Abfragen leicht und lesbar erstellen und performant ausführen. Hier zeigen sich deutlich die Vorteile einer Graphendatenbank, und es bleibt abzuwarten, ob Graphendatenbanken in der Zukunft mehr Bedeutung, u. a. für dieses Thema, zugesprochen wird.

Aufmacherbild: Marine knot on blue wooden background via Shutterstock / Urheberrecht: vetre

Verwandte Themen:

Geschrieben von
Daniel Brenner
Daniel Brenner
Daniel Brenner ist Consultant beim IT-Beratungsunternehmen BridgingIT GmbH. Er arbeitet aktuell bei einem Versorgungsunternehmen in einem internationalen Projekt zu E-Mobilität.
Stefan Zilch
Stefan Zilch
Stefan Zilch ist als Senior Consultant und Software Engineer beim IT Beratungsunternehmen BridgingIT GmbH angestellt und schon seit mehr als zehn Jahren in der Softwareentwicklung mit Java und JEE tätig. Seinen Fokus legt er dabei gerne auf Architektur- und Designthemen.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: